Java并发编程之重入锁(ReentrantLock)
简介
ReentrantLock重入锁,由于它的加锁和解锁操作需要手动来完成,所以也称为显式锁。
ReentrantLock是基于AQS独占模式实现的独占锁,同时只能有一个线程能获取到该锁,获取不到锁的线程将被放入该锁的AQS阻塞队列中等待。
ReentrantLock的简单使用
一个ReentrantLock使用示例,对临界资源进行加锁,当线程访问临界资源时需要先获得锁,如果锁已经被其他线程所持有,则需要等待其他线程释放锁后再重新获取锁,直到获取锁之后才能继续执行。
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable runnable = () -> {
try{
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获得了锁");
Thread.sleep(3000);
}catch (InterruptedException ex){
ex.printStackTrace();
}finally {
reentrantLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
/*
* 输出结果:
* Thread-1获得了锁
* Thread-1释放了锁
* Thread-0获得了锁
* Thread-0释放了锁
*/
可重入性
可重入性是指线程获得锁之后,没有释放锁,当再次需要获得同一把锁的时候仍然可以成功。如果锁是不可重入的,第二次获得锁的时候就会导致死锁。
state变量记录了锁的重入次数,每次lock()方法的调用会使state加1,每次unlock()方法的调用会使state减1,当state为0时锁被释放,将锁的持有者设置为null。
如下示例,多次调用lock()方法是可以的,但是对应的unlock()方法也要调用对应次数。
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable runnable = () -> {
try{
reentrantLock.lock();
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获得了锁");
Thread.sleep(3000);
}catch (InterruptedException ex){
ex.printStackTrace();
}finally {
reentrantLock.unlock();
reentrantLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
/*
* 输出结果:
* Thread-1获得了锁
* Thread-1释放了锁
* Thread-0获得了锁
* Thread-0释放了锁
*/
当unlock()方法次数少于lock()方法调用次数时,state变量值大于0,相当于当前线程还持有该锁,其他线程依然不能获取到锁,只能继续等待。如下所示:
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable runnable = () -> {
try{
reentrantLock.lock();
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获得了锁");
Thread.sleep(3000);
}catch (InterruptedException ex){
ex.printStackTrace();
}finally {
reentrantLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
/*
* 输出结果:
* Thread-1获得了锁
* Thread-1释放了锁
*/
当unlock()方法次数大于lock()方法调用次数时,相当于锁已经被释放,锁的持有者不再是当前线程,此时再次调用unlock()方法将抛出IllegalMonitorStateException异常。
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable runnable = () -> {
try{
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获得了锁");
Thread.sleep(3000);
}catch (InterruptedException ex){
ex.printStackTrace();
}finally {
reentrantLock.unlock();
reentrantLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了锁");
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}
输出结果:
Thread-0获得了锁
Thread-1获得了锁
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
at java.base/java.lang.Thread.run(Thread.java:834)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.base/java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:149)
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1302)
at java.base/java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:439)
at com.buhe.demo.demos.reen.ReentrantLockDemo.lambda$main$0(ReentrantLockDemo.java:18)
at java.base/java.lang.Thread.run(Thread.java:834)
公平策略
使用无参构造函数创建的ReentrantLock默认是非公平锁,带参数的构造函数可以指定公平策略,true表示公平锁,false表示非公平锁。
公平锁是指先请求锁的线程一定先获得锁,反之非公平锁则是随机的。
示例:
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
Runnable runnable = () -> {
for(int i = 0; i < 2; i++) {
try {
reentrantLock.lock();
System.out.println(Thread.currentThread().getName() + "获得了锁");
Thread.sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
锁的获取是随机的,即使刚释放锁的线程也有可能立即获得锁。
输出结果:
Thread-0获得了锁
Thread-0获得了锁
Thread-1获得了锁
Thread-1获得了锁
Thread-2获得了锁
Thread-2获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-4获得了锁
Thread-3获得了锁
修改ReentrantLock的创建方式,改为公平锁
ReentrantLock reentrantLock = new ReentrantLock(true);
先请求锁的线程一定先获得锁,锁的获取是有序的。
输出结果:
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁
Thread-2获得了锁
Thread-1获得了锁
Thread-0获得了锁
Thread-3获得了锁
Thread-4获得了锁
使用公平锁不会产生饥饿现象,由于要维护线程的顺序,使公平锁的效率低下,所以没特殊情况应该使用非公平锁。
和synchronized的比较
synchronized和ReentrantLock都属于独占锁,两者性能相差不大,但是在使用上,ReentrantLock要更灵活。
-
synchronized的加锁和解锁是自动进行的,易于操作,但不够灵活。ReentrantLock的加锁和解锁是手动进行的,不易操作,但更灵活。
-
ReentrantLock支持公平锁和非公平锁,而synchronized只支持非公平锁。
-
ReentrantLock可以关联多个条件队列,而synchronized最多只能关联一个条件队列。
-
在等待锁的过程中,使用ReentrantLock能使等待的线程响应中断,而使用synchronized时,线程只能保持等待直到获取到锁后继续执行。
-
ReentrantLock支持锁申请等待超时,当在给定的时间内还没有获取到锁,则主动放弃获取锁。
-
ReentrantLock支持尝试获取锁,如果锁没有被其他线程占用,则获得锁成功并立即返回true,如果锁被其他线程所占用,则锁获取失败直接返回false,不会进行等待。
-
ReentrantLock和synchronized都是可重入锁。
源码分析
ReentrantLock和AQS的关系如下图:
ReentrantLock的内部类Sync继承自AQS,它的子类NonfairSync和FairSync分别实现了非公平锁和公平锁策略。
通过ReentrantLock的构造函数参数来指定公平策略:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
下面以非公平锁为例分析下加锁和解锁的过程。
加锁
非公平锁加锁调用的是NonfairSync的lock方法,默认state为0,通过CAS将state设置为1,设置成功则表示加锁成功,并设置当前线程为锁的持有者。设置失败则调用AQS的acquire方法。
//java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
//CAS设置状态值
if (compareAndSetState(0, 1))
//设置当前线程为锁的持有者
setExclusiveOwnerThread(Thread.currentThread());
else
//调用AQS的acquire方法
acquire(1);
}
acquire方法,当tryAcquire方法返回false时,会把当前线程放入AQS阻塞队列。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法由AQS的子类实现,这里就是Sync以及它的子类来实现。
tryAcquire方法中会判断state值,如果state为0,则会尝试获取锁,使用CAS设置state的值,设置成功返回true,设置失败返回false。
如果state大于0,则判断持有锁的线程是不是当前线程,如果是则需要更新重入次数,重新设置state的值后返回true,反之则表示锁已经被其他线程所持有,直接返回false。
//java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁的状态值
int c = getState();
//判断锁的状态,为0则表示锁没有被任何线程所持有
if (c == 0) {
//CAS设置状态值
if (compareAndSetState(0, acquires)) {
/设置当前线程为锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
//判断持有锁的线程是否为当前线程
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
解锁
非公平锁和公平锁的解锁过程是一致的,解锁unlock方法调用的是AQS的release方法,release方法会调用tryRelease方法尝试释放锁,释放锁成功之后的处理由AQS完成。同tryAcquire方法,tryRelease方法也是需要AQS子类实现的。
//java.util.concurrent.locks.AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
//减少重入次数
int c = getState() - releases;
//如果不是锁的持有者调用unlock方法则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果重入次数为0,则清空锁的持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果当前持有锁的线程不是当前线程则直接抛出异常,否则继续判断state的值,如果state为0,则清空锁的持有线程,如果state不为0,则减少重入次数。
参考资料:
《Java并发编程之美》
一文彻底理解ReentrantLock可重入锁的使用