在JDK中独占锁的实现除了内部锁Synchronized,还可以使用ReentrantLock。
内部锁与可重入锁的对比
- 在JDK1.6以后,性能上两者差距已经不明显,因为对synchronized底层进行了优化,很多地方采用了CAS操作。在Java虚拟机上更偏向于内部锁的使用,官方也提倡在满足条件的情况下,优先选择内部锁。
- 两者都是独占锁,只允许一个线程进入临界区。synchronized的加锁释放锁的操作是隐式的,使用简单,但不够灵活,一般并发场景使用synchronized关键字可以满足需求;ReetrantLock需要手动加锁释放锁,释放锁的代码要在finally中代码块中,保证线程可以释放锁。在复杂并发场景中,可以使用ReentrantLock。
- 两者都是可重入锁。内部锁可以放在递归方法上,不用担心线程最后是否释放锁,ReentrantLock在重入时需要确保重复获取锁的次数和重复释放锁的次数一样,否则会导致其他线程无法获得锁。
- ReentrantLock可以实现公平锁,可响应中断和限时等待。
公平锁与非公平锁
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。在创建ReentrantLock的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁。
private static ReentrantLock lock = new ReentrantLock(true); //公平锁
// private static ReentrantLock lock = new ReentrantLock(false); //公平锁
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new FairSyncDemo(i)).start();
}
}
static class FairSyncDemo implements Runnable{
Integer id;
public FairSyncDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 2; i++) {
lock.lock();
System.out.println("获得锁的线程:"+id);
lock.unlock();
}
}
}
//公平锁运行结果
获得锁的线程:2
获得锁的线程:0
获得锁的线程:3
获得锁的线程:1
获得锁的线程:4
获得锁的线程:2
获得锁的线程:0
获得锁的线程:3
获得锁的线程:1
获得锁的线程:4
//非公平锁运行结果
获得锁的线程:3
获得锁的线程:0
获得锁的线程:0
获得锁的线程:4
获得锁的线程:4
获得锁的线程:3
获得锁的线程:1
获得锁的线程:1
获得锁的线程:2
获得锁的线程:2
线程会重复获取锁。如果申请获取锁的线程足够多,那么可能会造成某些线程长时间得不到锁。这就是非公平锁的“饥饿”问题。大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。
可响应中断
当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()
。该方法可以用来解决死锁问题。
获取锁时限时等待
ReentrantLock还给我们提供了获取锁限时等待的方法tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
Condition
ReentrantLock结合Condition接口同样可以实现等待通知机制。而且相比Synchronized使用起来更清晰也更简单。
public class ReentrantLockTest {
static BlockingQueue<Integer> queue = new BlockingQueue(5);
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
test2();
}
//测试queue
public static void test2() {
for (int i = 0; i < 10; i++) {
int data = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
static class BlockingQueue<E> {
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
List<E> list = new LinkedList<>();
int size;
public BlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() >= size)
notFull.await();
list.add(e);
System.out.println("进队列" + e);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
lock.lock();
try {
while (list.size() == 0)
notEmpty.await();
E e = list.remove(0);
System.out.println("出队列" + e);
notFull.signal();
return e;
} finally {
lock.unlock();
}
}
}
}