目录
读写锁
介绍
之前提到锁(如
CustomLock
和ReentrantLock
)基本都是独占锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问。但是在写线程访问时,所有的读线程和其他写线程均被阻塞
。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的独占锁有了很大提升。除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的所有读服务可见。
在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠 synchronized 关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。
改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。
当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行
,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。一般情况下,读写锁的性能都会比独占锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比独占锁更好的并发性和吞吐量
。其中的读锁就相当于下面的共享锁,写锁就相当于下面的独占锁。共享锁访问时,阻塞所有独占锁,独占锁访问时,阻塞其它独占锁和所有共享锁。
Java 并发包提供读写锁的实现是
ReentrantReadWriteLock
,它提供的特性如下表所示:
简单使用
ReentrantReadWriteLock
提供了获取读锁和写锁的两个方法,即readLock()
方法和writeLock()
方法。下面是一个读写锁的使用案例:
public class ReadWriteLockTest {
public static void main(String[] args) {
MyCacheLock myCache = new MyCacheLock();
// 写操作
for (int i = 1; i <= 6; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp + "", "akieay" + temp);
}, "write thread-"+i).start();
}
//读操作
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
Object o = myCache.get(temp + "");
}, "read thread="+i).start();
}
}
}
/**
* 加锁的自定义缓存
*/
class MyCacheLock {
private volatile Map<String, Object> map = new HashMap<>();
/**
* 读写锁:更加细粒度的控制
*/
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 读锁
*/
private Lock read = lock.readLock();
/**
* 写锁
*/
private Lock write = lock.writeLock();
/**
* 存储,写入的时候,只希望同时只有一个线程写
* @param key
* @param value
*/
public void put(String key, Object value) {
write.lock();
try {
System.out.println(Thread.currentThread().getName() + " 写入" + key);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入Ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
write.unlock();
}
}
/**
* 读取,读取的时候,所有人都可以读
* @param key
* @return
*/
public Object get(String key) {
read.lock();
Object value = null;
try {
System.out.println(Thread.currentThread().getName() + " 读取" + key);
value = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读取Ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
read.unlock();
}
return value;
}
}
运行程序,查看控制台打印:可以看到在当前线程获取写锁执行写操作时,不会有其他写操作和读操作执行,只有在当前写操作执行完成后,其它线程的写操作才能获取写锁并执行。在当前线程获取读锁执行读操作时,其它线程也可以获取读锁执行读操作【如示例中的读线程3在执行时,读线程4、5也在执行。】,其它执行读操作的线程并不会阻塞。
上述示例中,
MyCacheLock
使用一个非线程安全的HashMap
作为缓存的实现,同时使用读写锁的读锁和写锁来保证MyCacheLock
是线程安全的。在读操作get(String key)
方法中,需要获取读锁,读锁使得并发访问该方法时不会被阻塞。写操作put(String key, Object value)
方法,在更新HashMap
时必须提前获取写锁,当获取写锁后,其它线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其它线程的读写操作才能继续。MyCacheLock
使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。
读写锁的实现原理
要分析
ReentrantReadWriteLock
的实现原理,主要是分析以下几点:
读写状态的设计
写锁的获取与释放
读锁的获取与释放
锁降级
读写状态的设计
读写锁同样依赖自定义同步器【实现 AQS】来实现同步功能,而读写状态就是其同步器的同步状态。回想
ReentrantLock
中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态
,使得该状态的设计成为读写锁实现的关键。如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”
这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如下图所示【int 占 4 个字节,共 32 位】:
上面的同步状态表示一个线程已经获取了写锁,且重进入了两次【值为 3】,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为 S,写状态等于
S&0x0000FFFF
(将高16位全部抹去,0x0000FFFF
可由(1 << 16) -1
得到,左移右边补0),读状态等于S>>>16
(左边补0右移16位,将高16位抹去)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是 S+0x00010000 。根据状态的划分能得出一个推论【下面用得到】:S不等于0 且 写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
写锁的获取与释放
写锁的获取
写锁是一个支持重进入的
独占锁
。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,ReentrantReadWriteLock
获取写锁的源码如下:
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取整体的同步状态【包括读锁、写锁】
int c = getState();
// 获取写锁
int w = exclusiveCount(c);
// 同步状态不为0【即存在读锁或写锁中至少一种】
if (c != 0) {
// 写锁状态为0,即存在读锁【总状态不为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;
}
/* 执行到这里表示不存在写锁与读锁 */
/// 有资格获取写锁但被阻塞,或 CAS 修改同步状态失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置当前线程获取同步状态
setExclusiveOwnerThread(current);
return true;
}
该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取。原因在于:
读写锁要确保
写锁的操作对读锁可见
,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取。而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
另外注意
exclusiveCount
方法的实现,前面也介绍了:写状态等于S&0x0000FFFF
(将高16位全部抹去,左移右边补0),而0x0000FFFF
可由(1 << 16) -1
得到,所以这个方法是用来获取写锁同步状态的。
写锁的释放
写锁的释放与
ReentrantLock
的释放过程基本类似,每次释放均减少写状态;当写状态为 0 时,表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 写锁状态为低16位,可以直接减去
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁的获取与释放
读锁的获取
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在
ThreadLocal
中,由线程自身维护,这使获取读锁的实现变得复杂。获取读锁的核心方法tryAcquireShared
源码如下:
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取整体的同步状态【包括读锁、写锁】
int c = getState();
// 写锁已经被获取并且获取写锁的线程不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
// 读锁获取失败
return -1;
// 获取读锁状态
int r = sharedCount(c);
// 有资格获取读锁且没被阻塞 且 读锁的数量未达到最大值 且 CAS 修改读锁状态成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
// 设置 第一个获得读锁的线程
firstReader = current;
// 设置 firstReader保持计数
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader的保持计数自增1
firstReaderHoldCount++;
} else {
// 缓存的保持计数器,维护了每个线程的读锁获取次数
HoldCounter rh = cachedHoldCounter;
// 若计数器为空 或 计数器的Tid 不是当前线程的ID
if (rh == null || rh.tid != getThreadId(current))
// 将缓存的保持计数器 的值修改为 当前线程持有的读锁的数量
cachedHoldCounter = rh = readHolds.get();
// 若读锁数量为0
else if (rh.count == 0)
// 设置保持计数器
readHolds.set(rh);
// 读锁数量+1
rh.count++;
}
return 1;
}
// CAS 获取读锁失败时调用
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
// 计数器
HoldCounter rh = null;
// 自旋
for (;;) {
// 获取整体的同步状态【包括读锁、写锁】
int c = getState();
// 若存在写锁
if (exclusiveCount(c) != 0) {
// 若当前线程不是获取锁的线程
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
// 确保我们没有以重入的方式获取读锁
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 读锁数量达到最大限制
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS 更新读锁数量
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
在
tryAcquireShared(int unused)
方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS
保证)增加读状态,成功获取读锁。注意:更新同步状态需要加上 SHARED_UNIT (即:
(1 << 16)
,也就是0x00010000
),表示读锁数量 + 1【读锁为高 16 位】。
读锁的释放
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)【读锁为高 16 位】。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 获取读锁的计数器
HoldCounter rh = cachedHoldCounter;
// 若计数器为空,或计数器的Tid 不是 当前线程ID
if (rh == null || rh.tid != getThreadId(current))
// 当前线程持有的读锁的计数器
rh = readHolds.get();
// 当前线程持有的读锁的数量
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
// 读锁数量-1
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
// 更新同步状态
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
锁降级
锁降级指的是
写锁降级成为读锁
。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程
。接下来看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。示例如下:
class CachedData {
String data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 必须在获取写锁之前释放读锁
rwl.readLock().unlock();
// 获取写锁
rwl.writeLock().lock();
try {
// 重新检查状态,因为另一个线程可能已经获得了写锁并在我们之前更改了状态。
if (!cacheValid) {
// 准备数据的流程(略)
data = ....
cacheValid = true;
}
// 通过在释放写锁之前获取读锁来降级
rwl.readLock().lock();
} finally {
// 释放写锁,仍然保持读锁
rwl.writeLock().unlock();
}
}
try {
// 使用据的流程(略)
use(data);
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
上述示例中,当数据发生变更后,
cacheValid
变量(布尔类型且volatile
修饰)被设置为 true,此时所有访问processCachedData()
方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()
方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。锁降级中读锁的获取是否必要呢?
答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程 T )获取了写锁并修改了数据,那么当前线程无法感知线程 T 的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程 T 才能获取写锁进行数据更新。
LockSupport 工具类
当需要阻塞或唤醒一个线程的时候,可以使用
LockSupport
工具类来完成相应工作。LockSupport
定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport
也成为构建同步组件的基础工具。LockSupport
定义了一组以park
开头的方法用来阻塞当前线程,以及unpark(Thread thread)
方法来唤醒一个被阻塞的线程。这些方法如下:
在 Java 6 中,
LockSupport
增加了park(Object blocker)
、parkNanos(Object blocker,long nanos)
和parkUntil(Object blocker,long deadline)
3个方法,用于实现阻塞当前线程的功能,其中参数blocker
是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。使用示例:
public class LockSupportDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));
System.out.println(Thread.currentThread().getName() + "被唤醒");
}, "akieay-one");
thread.start();
String flag = "flag";
Thread thread2 = new Thread(() -> {
LockSupport.parkNanos(flag, TimeUnit.SECONDS.toNanos(10));
System.out.println(Thread.currentThread().getName() + "被唤醒");
}, "akieay-two");
thread2.start();
}
}
Condition 接口
Condition 介绍
任意一个 Java 对象,都拥有一组监视器方法(定义在
java.lang.Object
上),主要包括wait()
、wait(long timeout)
、notify()
以及notifyAll()
方法,这些方法与synchronized
同步关键字配合,可以实现等待/通知模式。Condition
接口也提供了类似Object
的监视器方法,与Lock
配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。通过对比Object
的监视器方法和Condition
接口,可以更详细地了解Condition
的特性,如下图:
Condition 使用示例
Condition
定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition
对象关联的锁。Condition
对象是由Lock
对象调用newCondition()
方法创建出来的,换句话说,Condition
是依赖Lock
对象的。一般都会将
Condition
对象作为成员变量。当调用await()
方法后,当前线程会释放锁并在此等待,而其他线程调用Condition
对象的signal()
方法,通知当前线程后,当前线程才从await()
方法返回,并且在返回前已经获取了锁。
Condition
定义的部分方法以及描述如下表:
获取一个
Condition
必须通过Lock
的newCondition()
方法。下面通过一个有界队列的示例来深入了解Condition
的使用方式。有界队列是一种特殊的队列,当队列为空时,队列的获取操作将会阻塞获取线程,直到队列中有新增元素,当队列已满时,队列的插入操作将会阻塞插入线程,直到队列出现 “空位”。以下为一个使用示例:
public class ConditionTest {
public static void main(String[] args) {
BoundedQueue boundedQueue = new BoundedQueue(4);
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
String remove = (String) boundedQueue.remove();
System.out.println(Thread.currentThread().getName() + " 消费产品 " + remove);
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "consumer").start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
String product = "akieay-" + i;
System.out.println(Thread.currentThread().getName() + " 生产产品 " + product);
boundedQueue.add(product);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "provider").start();
}
}
class BoundedQueue<T> {
private Object[] items;
/**
* 添加的下标
*/
private int addIndex;
/**
* 删除的下标
*/
private int removeIndex;
/**
* 数组当前数量
*/
private int count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
/**
* 添加一个元素,如果数组满,则添加线程进入等待状态,直到有"空位"
* @param t
* @throws InterruptedException
*/
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
System.out.println(Thread.currentThread().getName()+"----等待--");
// 阻塞等待
notFull.await();
}
items[addIndex] = t;
if (++addIndex == items.length) {
addIndex = 0;
}
++count;
// 通知消费
notEmpty.signal();
} finally {
lock.unlock();
}
}
/**
* 由头部删除一个元素,如果数组空,则删除线程进入等待状态,直到有新添加元素
* @return
* @throws InterruptedException
*/
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
System.out.println(Thread.currentThread().getName()+"----等待--");
// 阻塞等待
notEmpty.await();
}
Object x = items[removeIndex];
if (++removeIndex == items.length) {
removeIndex = 0;
}
--count;
// 通知生产
notFull.signal();
return (T) x;
} finally {
lock.unlock();
}
}
}
上述示例中,
BoundedQueue
通过add(T t)
方法添加一个元素,通过remove()
方法移出一个元素。以添加方法为例,首先需要获得锁,目的是确保数组修改的可见性和排他性。当元素数量等于数组长度时,表示数组已满,则调用notFull.await()
,当前线程随之释放锁并进入等待状态。如果元素数量不等于数组长度,表示数组未满,则添加元素到数组中,同时通知阻塞在notEmpty上
的线程,数组中已经有新元素可以获取。 在添加和删除方法中使用while
循环而非if
判断,目的是防止过早或意外的通知,只有条件符合才能够退出循环。回想之前提到的等待/通知的经典范式,二者是非常类似的。查看我们 main 方法中的逻辑来推断运行结果:
首先,我们启动类一个消费者线程,消费者每隔 2 秒消费一个产品;然后启动了一个生产者线程,生产者每隔 1 秒生产一个产品。由于先启动消费者,这时数组中还没有产品,此时消费者
等待
;生产者启动后,生产产品并通知
消费者消费,消费者被唤醒开始消费产品。由于产品的生产速度远快于消费速度,所以当数组满了后,生产者进入等待
状态,等待消费者消费产品后通知
生产者生产。运行程序,查看控制台打印信息如下:完全符合我们预期的结果。
Condition 的实现原理【重点】
ConditionObject
是同步器AbstractQueuedSynchronizer
的内部类,因为Condition
的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition
对象都包含着一个队列(以下称为等待队列),该队列是Condition
对象实现等待/通知功能的关键。下面将分析Condition
的实现,主要包括:等待队列、等待和通知,下面提到的Condition
如果不加说明均指的是ConditionObject
。
等待队列
等待队列是一个 FIFO 的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在
Condition
对象上等待的线程,如果一个线程调用了Condition.await()
方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中同步队列节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node
。一个Condition
包含一个等待队列,Condition
拥有首节点(firstWaiter
)和尾节点(lastWaiter
)。当前线程调用Condition.await()
方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图所示:【值得注意的是与同步队列不同的是 等待队列是一个单向队列,而同步队列是一个双向队列
】
如图所示,
Condition
拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter
指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS
保证,原因在于调用await()
方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。在
Object
的监视器模型上,一个对象拥有一个同步队列和等待队列;而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列【可以创建多个Condition
对象,每个都包含一个等待队列】,其对应关系如图所示:
如图所示,
Condition
的实现是同步器的内部类,因此每个Condition
实例都能够访问同步器提供的方法,相当于每个Condition
都拥有所属同步器的引用。
等待 await
调用
Condition
的await()
方法(或者以await
开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()
方法返回时,当前线程一定获取了Condition
相关联的锁。如果从队列(同步队列和等待队列)的角度看await()
方法,当调用await()
方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition
的等待队列中。Condition
的await()
方法源码如下:
public final void await() throws InterruptedException {
// 检查线程是否中断
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程加入等待队列尾部并返回其节点
Node node = addConditionWaiter();
// 释放同步状态,也就是释放锁,在释放的过程中会唤醒同步队列中的下一个节点
int savedState = fullyRelease(node);
int interruptMode = 0;
// 若当前节点不处于同步队列上
while (!isOnSyncQueue(node)) {
// 当前线程进入等待状态
LockSupport.park(this);
// 检查线程是否被中断,没有被中断则返回 0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 若当前节点的后继节点不为空
if (node.nextWaiter != null)
// 遍历等待队列节点并清除失效节点
unlinkCancelledWaiters();
// 处理被中断的情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
当等待队列中的节点被唤醒,则被唤醒节点的线程开始尝试获取同步状态。如果不是通过其他线程调用
Condition.signal()/signalAll()
方法唤醒,而是对等待线程进行中断而唤醒,则会抛出InterruptedException
。如果从队列的角度去看,当前线程加入
Condition
的等待队列,同步队列的首节点并不会直接加入等待队列,而是通过addConditionWaiter()
方法把当前线程构造成一个新的节点并将其加入等待队列中。关于上面的实现过程还有以下几个细节需要了解:
- 将当前线程添加到等待队列的过程
- 释放锁的过程
- 从
await
方法退出并获取Lock
锁的过程一、将当前线程添加到等待队列,核心方法为
addConditionWaiter
,其源码为:
private Node addConditionWaiter() {
// 获取等待队列尾节点
Node t = lastWaiter;
// 若存在尾节点,且尾节点状态不是 等待状态
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍历并释放等待队列中失效的节点
unlinkCancelledWaiters();
// 重新获取尾节点
t = lastWaiter;
}
// 将当前线程构造成一个节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 若尾节点为空,则证明等待队列为空
if (t == null)
// 设置首节点位当前节点
firstWaiter = node;
else
// 设置当前节点为当前尾节点的后继节点
t.nextWaiter = node;
// 设置新的尾节点
lastWaiter = node;
return node;
}
/** 遍历并释放等待队列中失效的节点 */
private void unlinkCancelledWaiters() {
// 获取等待队列头节点
Node t = firstWaiter;
Node trail = null;
// 循环遍历等待队列的节点
while (t != null) {
// 获取节点的后继节点
Node next = t.nextWaiter;
// 若节点状态不为 等待状态
if (t.waitStatus != Node.CONDITION) {
/* 删除失效节点 */
// 使节点的尾指针失效
t.nextWaiter = null;
if (trail == null)
// 设置等待队列头节点为其后继节点
firstWaiter = next;
else
trail.nextWaiter = next;
// 若节点已遍历完成
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
以上源码其主要逻辑是:首先获取等待队列的尾节点,若尾节点不处于等待状态【即尾节点失效】,则遍历并释放等待队列中的失效节点,然后重新获取尾节点【保证获取到的尾节点为等待状态 或为 null】;然后将当前线程构造成节点,且状态为等待状态;在判断尾节点是否为空,若尾节点为空【等待队列为空】,则将当前节点设置为头节点,若不为空,则将当前节点设置为原尾节点的后继节点,并将当前节点设置为尾节点并返回。
二、释放锁的过程,其核心方法为
fullyRelease
,源码如下:
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取同步状态
int savedState = getState();
// 释放同步状态
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
// 修改节点状态
node.waitStatus = Node.CANCELLED;
}
}
以上源码的主要逻辑是:获取同步状态,然后调用
AQS
的模板方法release
方法释放AQS
的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则返回获取到的同步状态,若失败的话就抛出异常且将节点状态设置为CANCELLED
【取消状态】。三、从
await
方法退出并获取Lock
锁的过程,重点关注await
方法的以下源码:
// 若当前节点不处于同步队列上
while (!isOnSyncQueue(node)) {
// 当前线程进入等待状态
LockSupport.park(this);
// 检查线程是否被中断,没有被中断则返回 0
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 若当前节点的后继节点不为空
if (node.nextWaiter != null)
// 遍历节点并清除失效节点
unlinkCancelledWaiters();
// 处理被中断的情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
分析以上源码可知:当线程第一次调用
await()
方法时,会进入到这个while()
循环中,然后通过LockSupport.park(this)
方法使得当前线程进入等待状态;想要退出await
方法第一个前提条件就是要先退出这个while
循环;退出方式有两种:
- 不满足
while
循环的条件,即:当且节点处于同步队列中- 满足
if
中的条件,从而break
退出,即:线程被中断总结一下就是,当前线程被中断,或者其它线程调用
condition.signal/condition.signalAll
方法,当前线程移动到同步队列后,即可退出while
循环;当退出循环后,会调用acquireQueued
尝试获取锁,该方法会通过CAS
自旋不断尝试获取同步状态,直至成功(线程获取到lock);这样当退出await
方法时,已经获得了condition
引用(关联)的Lock
。至此,关于
await
方法的流程及原理介绍完毕,下面是整个方法的流程图:
通知 signal
调用
Condition
的signal()
方法,将会唤醒在等待队列中等待时间最长的节点(头节点),在唤醒节点之前,会将节点移到同步队列中。Condition
的signal()
方法,源码如下:调用该方法的前置条件是当前线程必须获取了锁,可以看到
signal()
方法进行了isHeldExclusively()
检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport
唤醒节点中的线程。
public final void signal() {
// 当前线程若没有占有锁,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取等待队列头节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 将等待队列头节点的后继节点指定为新的头节点,并判断其是否为空
if ( (firstWaiter = first.nextWaiter) == null)
// 设置尾节点为空【等待队列不存在元素了】
lastWaiter = null;
// 清除节点的后继节点的指针【标识当前节点失效,可回收】
first.nextWaiter = null;
// 若 将节点从等待队列转移到同步队列失败 且 头节点不为空
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// CAS 修改节点的状态为初始状态,如果不能修改waitStatus,表示该节点已经取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将节点插入同步队列尾部,并返回其前驱节点
Node p = enq(node);
// 获取前驱节点状态
int ws = p.waitStatus;
// 若前驱节点状态 > 0 或 CAS 修改前驱节点状态【为 SIGNAL】失败
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 唤醒线程
LockSupport.unpark(node.thread);
return true;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
通过调用同步器的
enq(Node node)
方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport
唤醒该节点的线程。被唤醒后的线程,将从await()
方法中的while
循环中退出(isOnSyncQueue(Node node)
方法返回t rue,节点已经在同步队列中),进而调用同步器的acquireQueued()
方法加入到获取同步状态的竞争中。 成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()
方法返回,此时该线程已经成功地获取了锁。Condition
的signalAll()
方法,相当于对等待队列中的每个节点均执行一次signal()
方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。节点从等待队列移动到同步队列的流程图如下:
总结
Condition
的等待通知整体流程如下:
其中蓝色为:Thread 获取锁并调用
await
方法阻塞线程并释放锁。其中绿色为:Thread 获取锁并调用
signal
方法唤醒被阻塞的线程。其中程色为:Thread 获取锁失败,将线程构造成节点添加到同步队列,在其它线程释放锁后,参与锁竞争并获取锁后执行。
至此,关于
Condition
的介绍完毕,现在应该就能很容易的理解前面的使用示例了。