前言
本篇文章将对基于AbstractQueuedSynchronizer
实现的锁进行学习,同时对LockSupport
和Condition
的使用进行整理和分析。内容参考了《Java并发编程的艺术》第5章。在之前的多线程学习-队列同步器中已经对AbstractQueuedSynchronizer
的原理进行了详尽分析,如果不熟悉AbstractQueuedSynchronizer
,可以先查阅该篇文章。
参考资料:《Java并发编程的艺术》
正文
一. 重入锁
重入锁,即ReentrantLock
,继承于Lock
接口,提供锁重入功能。重入锁与不可重入锁的区别在于,重入锁支持已经获取锁的线程重复对锁资源进行获取,Java
中的synchronized
关键字可以隐式的支持锁重入功能,考虑如下一个例子。
public class HelloUtil {
public static synchronized void sayHello() {
System.out.print("Hello ");
sayWorld();
}
public static synchronized void sayWorld() {
System.out.println("World");
}
}
已知访问由synchronized
关键字修饰的静态方法时需要先获取方法所在类的Class
对象作为锁资源,所以当A线程调用HelloUtil
的sayHello()
方法时,需要获取的锁资源为HelloUtil
类的Class
对象,此时B线程再调用HelloUtil
的sayHello()
或sayWorld()
方法时会被阻塞,但是A线程却可以在sayHello()
方法中再调用sayWorld()
方法,即A线程在已经获取了锁资源的情况下又获取了一次锁资源,这就是synchronized
关键字对锁重入的支持。
结合上面的例子,已经对重入锁有了直观的认识,下面将分析ReentrantLock
是如何实现重入锁的。ReentrantLock
的类图如下所示。
ReentrantLock
有三个静态内部类,其中Sync
继承于AbstractQueuedSynchronizer
,然后FairSync
和NonfairSync
继承于Sync
,因此Sync
,FairSync
和NonfairSync
均是ReentrantLock
组件中的自定义同步器,且FairSync
提供公平获取锁机制,NonfairSync
提供非公平获取锁机制。公平和非公平获取锁机制现在暂且不谈,下面先看一下Sync
,FairSync
和NonfairSync
实现了哪些方法,如下所示。
NonfairSync
和FairSync
提供获取锁机制的不同就在于其实现的lock()
和tryAcquire()
方法的不同,具体是使用哪一种获取锁机制,是在创建ReentrantLock
时指定的,ReentrantLock
的构造函数如下所示。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
由上述可知,ReentrantLock
默认使用非公平获取锁机制,然后可以在构造函数中根据传入的fair参数决定使用哪种机制。现在先对上面的讨论做一个小节:ReentrantLock
是可重入锁,即已经获取锁资源的线程可以重复对锁资源进行获取,ReentrantLock
内部有三个自定义同步器,分别为Sync
,NonfairSync
和FairSync
,其中NonfairSync
和FairSync
能分别提供非公平获取锁机制和公平获取锁机制,具体使用哪一种获取锁机制,需要在ReentrantLock
的构造函数中指定。
接下来结合NonfairSync
和FairSync
的lock()
和tryAcquire()
方法的源码,对非公平获取锁机制和公平获取锁机制进行说明。
NonfairSync
的lock()
方法如下所示。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平获取锁调用lock()
方法时会先将state以CAS方式从0设置为1,设置成功表示竞争到了锁,因此非公平获取锁意味着同时获取锁资源时会存在竞争关系,不能满足先到先获取的原则。如果将state以CAS方式从0设置为1失败时,会调用模板方法acquire()
,已知acquire()
方法会调用tryAcquire()
方法,而NonfairSync
的tryAcquire()
方法会调用其父类Sync
的nonfairTryAcquire()
方法,下面看一下其实现。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程如果是获取到锁资源的线程,则将state字段加1
//当前线程如果不是获取到锁资源的线程,则返回false然后加入同步队列
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
在nonfairTryAcquire()
方法中主要是对获取锁资源的线程进行判断,如果当前线程就是已经获取到锁资源的线程,那么就会将state加1,因为每次都是将state加1,所以可以重复获取锁资源。
接下来再看一下公平获取锁机制的FairSync
的实现,首先FairSync
的lock()
方法会直接调用模板方法acquire()
,并已知在acquire()
方法中会调用tryAcquire()
方法,所以这里直接看FairSync
的tryAcquire()
方法的实现。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
FairSync
的tryAcquire()
方法与NonfairSync
的不同在于当state为0时多了一个hasQueuedPredecessors()
方法的判断逻辑,即判断当前的同步队列中是否已经有正在等待获取锁资源的线程,如果有,则返回true,因此公平获取锁意味着绝对时间上最先请求锁资源的线程会最先获取锁,以及等待获取锁资源时间最长的线程会最优先获取锁,这样的获取锁机制就是公平的。
现在最后分析一下ReentrantLock
的解锁逻辑。无论是非公平获取锁机制还是公平获取锁机制,如果重复对锁资源进行了n次获取,那么成功解锁就需要对锁资源进行n次释放,前(n - 1)次释放锁资源都应该返回false。ReentrantLock
的unlock()
方法会直接调用AbstractQueuedSynchronizer
的模板方法release()
,并已知在release()
方法中会调用tryRelease()
方法,这里调用的是Sync
实现的tryRelease()
方法,如下所示。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease()
方法中每成功释放一次锁资源,就会将state减1,所以当state为0时,就判断锁资源被全部释放,即释放锁资源成功。
二. 读写锁
读写锁,即ReentrantReadWriteLock
,同一时刻可以允许多个读线程获取锁,但当写线程获取锁后,读线程和其它写线程应该被阻塞。下面以一个单例缓存的例子来说明ReentrantReadWriteLock
的使用。
public class Cache {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock rLock = rwLock.readLock();
private final Lock wLock = rwLock.writeLock();
private final Map map = new HashMap<>();
private static Cache instance = null;
private Cache() {}
public Cache getCacheInstance() {
if (instance == null) {
synchronized (Cache.class) {
if (instance == null) {
instance = new Cache();
}
}
}
return instance;
}
public String getValueByKey(String key) {
rLock.lock();
try {
return map.get(key);
} finally {
rLock.unlock();
}
}
public void addValueByKey(String key, String value) {
wLock.lock();
try {
map.put(key, value);
} finally {
wLock.unlock();
}
}
public void clearCache() {
wLock.lock();
try {
map.clear();
} finally {
wLock.unlock();
}
}
}
根据例子可知,ReentrantReadWriteLock
提供了一对锁:写锁和读锁,并且使用规则如下。
- 当前线程获取读锁时,读锁是否被获取不会影响读锁的获取;
- 当前线程获取读锁时,若写锁未被获取或者写锁被当前线程获取,则允许获取读锁,否则进入等待状态;
- 当前线程获取写锁时,若读锁已经被获取,无论获取读锁的线程是否是当前线程,都进入等待状态;
- 当前线程获取写锁时,若写锁已经被其它线程获取,则进入等待状态。
下面将结合源码对写锁和读锁的获取和释放进行分析。首先看一下ReentrantReadWriteLock
的类图。
ReentrantReadWriteLock
一共有五个内部类,分别为Sync
,FairSync
,NonfairSync
,WriteLock
和ReadLock
,同时可以看到,只有WriteLock
和ReadLock
实现了Lock
接口,因此ReentrantReadWriteLock
的写锁和读锁的获取和释放实际上是由WriteLock
和ReadLock
来完成,所以这里对ReentrantReadWriteLock
的工作原理进行一个简单概括:ReentrantReadWriteLock
的写锁和读锁的获取和释放分别由其内部类WriteLock
和ReadLock
来完成,而WriteLock
和ReadLock
对同步状态的操作又是依赖于ReentrantReadWriteLock
实现的三个自定义同步器Sync
,FairSync
和NonfairSync
。
下面继续分析写锁和读锁的同步状态的设计。通过上面的分析可以知道WriteLock
和ReadLock
依赖同一个自定义同步组件Sync
,因此WriteLock
和ReadLock
对同步状态进行操作时会修改同一个state变量,即需要在同一个整型变量state上维护写锁和读锁的同步状态,而Java
中整型变量一共有32位,所以ReentrantReadWriteLock
将state的高16位表示读锁的同步状态,低16位表示写锁的同步状态。鉴于读写锁同步状态的设计,对读写锁同步状态的运算操作归纳如下。
- 获取写锁同步状态: state & 0x0000FFFF
- 获取读锁同步状态: state >>> 16
- 写锁同步状态加一: state + 1
- 读锁同步状态加一: state + (1 << 16)
理清楚了ReentrantReadWriteLock
的组件之间的关系和读写锁同步状态的设计之后,下面开始分析写锁和读锁的获取和释放。
1. 写锁的获取
WriteLock
的lock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法acquire()
,在acquire()
方法中会调用自定义同步器Sync
重写的tryAcquire()
方法,下面看一下tryAcquire()
方法的实现。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//c表示state
int c = getState();
//w表示写锁同步状态
int w = exclusiveCount(c);
if (c != 0) {
//state不为0,但是写锁同步状态为0,表示读锁已经被获取
//获取写锁时只要读锁被获取过,就不允许获取写锁
//因为写锁是独占锁,所以持有写锁的线程不是当前线程也不允许获取写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//执行到这里表示写锁重入
setState(c + acquires);
return true;
}
//非公平获取锁时writerShouldBlock()返回false
//公平获取锁时writerShouldBlock()会调用hasQueuedPredecessors()方法
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上述tryAcquire()
方法中,在获取写锁之前会判断读锁是否被获取以及写锁是否被其它线程获取,任意一个条件满足都不允许当前线程获取写锁。同时如果写锁和读锁均没有被获取,即state为0时,还会调用writerShouldBlock()
方法来实现非公平或公平锁的语义,如果是非公平锁,writerShouldBlock()
方法会返回false,此时当前线程会以CAS方式修改state,修改成功则表示获取读锁成功,如果是公平锁,writerShouldBlock()
方法会调用hasQueuedPredecessors()
方法来判断同步队列中是否已经有正在等待获取锁资源的线程,如果有,则当前线程需要加入同步队列,后续按照等待时间越久越优先获取锁的机制来获取写锁。
2. 写锁的释放
WriteLock
的unlock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法release()
,在release()
方法中会调用自定义同步器Sync
重写的tryRelease()
方法,下面看一下tryRelease()
方法的实现。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
因为写锁支持重入,所以在释放写锁时会对写锁状态进行判断,只有写锁状态为0时,才表示写锁被成功释放掉。
3. 读锁的获取
ReadLock
的lock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法acquireShared()
,在acquireShared()
方法中会调用自定义同步器Sync
重写的tryAcquireShared()
方法,tryAcquireShared()
方法并不完整,其最后会调用fullTryAcquireShared()
方法,该方法的注释说明如下。
获取读锁同步状态的完整版本,能够实现在
tryAcquireShared()
方法中未能实现的
CAS设置状态失败重试和读锁重入的功能。
在JDK1.6
中ReentrantReadWriteLock
提供了getReadHoldCount()
方法,该方法用于获取当前线程获取读锁的次数,因为该方法的加入,导致了读锁的获取的逻辑变得更为复杂,下面将结合tryAcquireShared()
和fullTryAcquireShared()
方法的实现,在抛开为实现getReadHoldCount()
方法功能而新增的逻辑的情况下,给出读锁获取的简化实现代码。
final int fullTryAcquireShared(Thread current) {
for (;;) {
//c表示state
int c = getState();
//如果写锁被获取并且获取写锁的线程不是当前线程,则不允许获取读锁
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//安全的将读锁同步状态加1
if (compareAndSetState(c, c + SHARED_UNIT))
return 1;
}
}
由上述可知,读锁在写锁被获取并且获取写锁的线程不是当前线程的情况下,不允许被获取,以及读锁的同步状态为所有线程获取读锁的次数之和。
4. 读锁的释放
ReadLock
的unlock()
方法直接调用了AbstractQueuedSynchronizer
的模板方法releaseShared()
,在releaseShared()
方法中会调用自定义同步器Sync
重写的tryReleaseShared()
方法,该方法同样在JDK1.6
中加入了较为复杂的逻辑,下面给出其简化实现代码。
protected final boolean tryReleaseShared(int unused) {
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
由上述可知,只有在state为0时,即读锁和写锁均被释放的情况下tryReleaseShared()
方法才会返回true,在官方的注释中给出了这样设计的原因,如下所示。
释放读锁对读线程没有影响,但是当读锁和写锁均被释放的情况下,在同步队列中等待的写线程就有可能去获取写锁。
三. Condition接口
Condition
接口定义了一组方法用于配合Lock
实现等待/通知模式,与之作为对比的是,用于配合synchronized
关键字实现等待/通知模式的定义在java.lang.Object
上的监视器方法wait()
和notify()
等。《Java并发编程的艺术》5.6小节对两者的差异进行了对比和总结,这里直接贴过来作参考。
对比项 | Object Monitor Methods | Condition |
---|---|---|
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态,等待过程中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并等待至将来某个时间点 | 不支持 | 支持 |
唤醒队列中的一个线程 | 支持 | 支持 |
唤醒队列中的多个线程 | 支持 | 支持 |
通常基于Lock
的newCondition()
方法创建Condition
对象并作为对象成员变量来使用,如下所示。
public class MyCondition {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
......
}
队列同步器AbstractQueuedSynchronizer
的内部类ConditionObject
实现了Condition
接口,后续将基于ConditionObject
的实现进行讨论。首先给出Condition
接口定义的方法。
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
上述方法的说明如下表所示。
方法 | 说明 |
---|---|
await() |
调用此方法的线程进入等待状态,响应中断,也可以被signal() 和signalAll() 方法唤醒并返回,唤醒并返回前需要获取到锁资源。 |
awaitUninterruptibly() |
同await() ,但不响应中断。 |
awaitNanos() |
同await() ,并可指定等待时间,响应中断。该方法有返回值,表示剩余等待时间。 |
awaitUntil() |
同await() ,并可指定等待截止时间点,响应中断。该方法有返回值,true表示没有到截止时间点就被唤醒并返回。 |
signal() |
唤醒等待队列中的第一个节点。 |
signalAll() |
唤醒等待队列中的所有节点。 |
针对上面的方法再做两点补充说明:
- 等待队列是
Condition
对象内部维护的一个FIFO队列,当有线程进入等待状态后会被封装成等待队列的一个节点并添加到队列尾; - 从等待队列唤醒并返回的线程一定已经获取到了与
Condition
对象关联的锁资源,Condition
对象与创建Condition
对象的锁关联。
下面将结合ConditionObject
类的源码来对等待/通知模式的实现进行说明。await()
方法的实现如下所示。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//基于当前线程创建Node并添加到等待队列尾
//这里创建的Node的等待状态为CONDITION,表示等待在等待队列中
Node node = addConditionWaiter();
//释放锁资源
int savedState = fullyRelease(node);
int interruptMode = 0;
//Node从等待返回后会被添加到同步队列中
//Node成功被添加到同步队列中则退出while循环
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//让Node进入自旋状态,竞争锁资源
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//遍历等待队列,将已经取消等待的节点从等待队列中去除链接
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//Node如果是被中断而从等待返回,则抛出中断异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
理解await()
方法的整个执行流程前,先看一下等待队列的一个示意图,如下所示。
Condition
对象分别持有等待队列头节点和尾节点的引用,新添加的节点会添加到等待队列尾,同时lastWaiter会指向新的尾节点。
现在回到await()
方法,在await()
方法中,会做如下事情。
- 首先,会基于当前线程创建
Node
并添加到等待队列尾,创建Node
有两个注意点:1. 这里创建的Node
复用了同步队列中的Node
定义;2. 在创建Node
前会判断等待队列的尾节点是否已经结束等待(即等待状态不为Condition),如果是则会遍历等待队列并将所有已经取消等待的节点从等待队列中去除链接; - 然后,当前线程会释放锁资源,并基于
LockSupport.park()
进入等待状态; - 再然后,当前线程被其它线程唤醒,或者当前线程被中断,无论哪种方式,当前线程对应的
Node
都会被添加到同步队列尾并进入自旋状态竞争锁资源,注意,此时当前线程对应的Node
还存在于等待队列中; - 再然后,判断当前线程对应的
Node
是否是等待队列尾节点,如果不是则触发一次清除逻辑,即遍历等待队列,将已经取消等待的节点从等待队列中去除链接,如果是等待队列尾节点,那么当前线程对应的Node
会在下一次创建Node
时从等待队列中被清除链接; - 最后,判断当前线程从等待返回的原因是否是因为被中断,如果是,则抛出中断异常。
上面讨论了等待的实现,下面再结合源码看一下通知的实现。首先是signal()
方法,如下所示。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
由signal()
方法可知,调用signal()
方法的线程需要持有锁,其次signal()
方法会唤醒等待队列的头节点,即可以理解为唤醒等待时间最久的节点。下面再看一下signalAll()
方法,如下所示。
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
可以发现,signalAll()
与signal()
方法大体相同,只不过前者最后会调用doSignalAll()
方法来唤醒所有等待节点,后者会调用doSignal()
方法来唤醒头节点,下面以doSignal()
方法进行说明。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
实际就是在transferForSignal()
方法中将头节点添加到同步队列尾,然后再调用LockSupport.unpark()
进行唤醒。
四. LockSupport
在本篇文章的最后,对java.util.concurrent.locks
包中的一个重要工具类LockSupport
进行说明。LockSupport
提供了一组静态方法用于阻塞/唤醒线程,方法签名如下所示。
public static void park()
public static void park(Object blocker)
public static void parkNanos(long nanos)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(long deadline)
public static void parkUntil(Object blocker, long deadline)
public static void unpark(Thread thread)
将LockSupport
的方法进行整理如下。
方法 | 说明 |
---|---|
park() |
将调用park() 方法的线程阻塞,响应中断。 |
parkNanos() |
将调用parkNanos() 方法的线程阻塞,并指定阻塞时间,响应中断。 |
parkUntil() |
将调用parkUntil() 方法的线程阻塞,并指定截止时间点,响应中断。 |
unpark(Thread thread) |
唤醒传入的线程。 |
现在已知让线程睡眠(阻塞或等待)的方式有四种,分别是Thread.sleep(time)
,LockSupport.park()
,Object.wait()
和Condition.await()
,在本篇文章的最后,对上述四种方式进行一个简单对比,如下表所示。
方式 | 说明 |
---|---|
Thread.sleep(time) |
调用该方法必须指定线程睡眠的时间,睡眠中的线程可以响应中断并抛出中断异常,调用该方法时不需要线程持有锁资源,但是持有锁资源的线程调用该方法睡眠后不会释放锁资源。 |
LockSupport.park() |
调用该方法的线程会被阻塞,被阻塞中的线程可以响应中断但不会抛出中断异常,调用该方法时不需要线程持有锁资源,但是持有锁资源的线程调用该方法睡眠后不会释放锁资源 |
Object.wait() |
调用该方法的线程会进入等待状态,等待状态中的线程可以响应中断并抛出中断异常,调用该方法时需要线程已经持有锁资源,调用该方法后会释放锁资源。 |
Condition.await() |
调用该方法的线程会进入等待状态,等待状态中的线程可以响应中断并抛出中断异常,调用该方法时需要线程已经持有锁资源,调用该方法后会释放锁资源。 |
总结
本篇文章主要对重入锁和读写锁的使用和原理进行了学习,重入锁和读写锁的实现均是基于队列同步器AbstractQueuedSynchronizer
。然后又对Condition
的使用和原理进行了学习,通过Condition
进入等待状态的线程会在等待队列上进行等待,被唤醒或中断时又会进入同步队列加入到对锁资源的竞争中,只有获取到了锁资源才能从等待状态返回。最后对LockSupport
工具类进行了一个简单说明,并针对线程进入睡眠的四种方式做了一个简单对比。