提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
J.U.C是JDK提供的并发工具包(Java.util.concurrent),里面提供了很多并发编程中实用工具类,本文主要讲解了为什么引入lock接口、AQS及Condition。
synchronized关键字虽然可以解决大部分多线程锁的问题,但是仍旧存在下述问题:
1、假如持有锁的某线程因等待长时IO或者其他原因阻塞,其他等待的线程无法响应中断,只能不断等待;
2、多线程下只有读操作是不会发生冲突的,但synchronized关键字对读和写操作均一视同仁,所以当一个线程进行读取操作时,其他线程只能不断等待;
3、使用synchronized关键字无法确认线程是否成功获取到锁。
针对上述问题,Doug Lea李大爷实现了一套更加灵活的Java锁机制,即J.U.C的locks包。
相比synchronized关键字,lock的好处:
1、增加超时时间设置,避免synchronized代码块中执行时间过长
2、提供tryLock()方法,可以尝试获取锁,如果获取不到可以转去做别的事情。
3、提供condition条件, 可以满足一些复杂的获取锁的场景。如先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D.
Lock有下述6个方法,主要分为三大类:
获取锁的方法,分别为lock()、lockInterruptibly()、tryLock()、tryLock(long, TimeUnit);
释放锁的方法,unlock();
线程协作相关的方法,newCondition()。
synchronsized关键字不需要用户手动释放锁,当synchronized修饰的方法或代码块执行完毕后,系统会自动让线程释放对锁的占用。
与synchronsized关键字不同的是,Lock必须由用户手动执行加锁/释放锁操作,当持有锁的线程发生异常时,该线程不会自动释放锁,可能会导致死锁,故Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
// 初始化锁对象
Lock lock = ...;
// 加锁操作
lock.lock();
try{
// 执行相应任务
doSomething();
}catch(Exception e){
// 处理异常
}finally{
// 释放锁
lock.unlock();
}
与lock()不同的是,tryLock()是由返回值的,获取到锁则返回true,否则返回false,tryLock(long, TimeUnit)为其重载方法,表示获取不到锁之后会等待一定时间,如果在时间期限内获取到锁,则返回true,否则返回false。
lockInterruptibly()方法,当线程获取不到锁,在等待的过程中是可以响应中断的。
不过需要注意的是,通过lockInterruptibly()方法获取到锁的线程,在运行过程中是不能响应中断的,仅是做一个中断标记,待释放锁之后再响应中断。
J.U.C包中Lock接口的实现类主要有5个:
除了ReentrantLock外,其他均为其他类的内部类,实际应用中如何使用ReentrantLock呢?
代码如下(示例):
public class Demo {
private static int count=0;
static Lock lock=new ReentrantLock();
public static void inc(){
lock.lock();
try {
Thread.sleep(1);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
这段代码主要做一件事,就是通过一个静态的incr()方法对共享变量count做连续递增,在没有加同步锁的情况下多线程访问这个方法一定会存在线程安全问题。所以用到了ReentrantLock来实现同步锁,并且在finally语句块中释放锁。
那么我来引出一个问题,大家思考一下:
多个线程竞争锁失败后是如何实现等待以及被唤醒的呢?
AQS,全名AbstractQueuedSynchronizer,是一个抽象类的队列式同步器,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
AQS的内部通过维护一个状态volatile int state
(共享资源),一个FIFO线程等待队列来实现同步功能。
state用关键字volatile修饰,代表着该共享资源的状态一更改就能被所有线程可见,而AQS的加锁方式本质上就是多个线程在竞争state,当state为0时代表线程可以竞争锁,不为0时代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,这些线程会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
用一张原理图来简单概括:
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断
共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等
独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等,它又可分为:
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。
在超类 AbstractOwnableSynchronizer 中只有一个属性:标识独占模式下当前所有者
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。该特性的实现需要解决一下两个问题:
1、线程再次进入:获取锁的线程如果是【当前占据锁的线程】,则再次成功获取。
2、锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行技术自增,技术表示当前锁被重复获取的次数,而锁被释放时,计数自减去,当技术为0时表示锁已经成功释放。
state表示资源的可用状态
因为ReentrantLock允许重入,重写了tryAcquire(),所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁.
因此,不同的AQS实现,state所表达的含义是不一样的。
State三种访问方式:
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
节点状态
- 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
- CANCELLED,值为1,表示当前的线程被取消;
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要唤醒,也就是unpark;
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。主要用于维护获取锁失败时入队的线程。
AQS 依赖CLH同步队列来完成同步状态的管理:
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:
调用await()的时候会释放锁,然后线程会加入到条件队列;
调用signal()/signalAll()唤醒的时候会把条件队列中的头节点移动到同步队列中,进行阻塞,等待再次获得锁
Condition接口:
1、调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
2、调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition #signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。
目的:
addWaiter 加入队列
acquireQueued 线程阻塞
selfInterrupt 响应线程中断【条件:没有获得锁,并且加入队列,完成线程阻塞】
public final void acquire(int arg) {
//判断是否是重入锁
if (!tryAcquire(arg) &&
//addWaiter加入队列
//acquireQueued线程阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//响应线程中断
selfInterrupt();
}
获取锁的特性,交由AQS实现类实现,以下以ReentrantLock为例:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 通过state状态
int c = getState();
// 如果没有线程获取锁
if (c == 0) {
//通过CAS操作获取锁,如果获取成功,则设置占用锁的线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判断是否是锁重入getExclusiveOwnerThread 方法返回占用锁的线程
else if (current == getExclusiveOwnerThread()) {
//state++
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置state的值
setState(nextc);
return true;
}
//如果锁已经被其他线程占用,返回false
return false;
}
// mode 传入Node.EXCLUSIVE 表示互斥锁 Node.SHARED 表示共享锁(读写锁中的读锁)
private Node addWaiter(Node mode) {
// 创建一个节点保存当前线程
Node node = new Node(Thread.currentThread(), mode);
//获取节点的尾节点。
//如果是线程B是第一个阻塞的节点,这里是空值,通过enq方法进行设置head节点和tail节点。
Node pred = tail;
if (pred != null) {
//如果线程C在线程B之后,线程C执行到这里,那么这里的pred是B
node.prev = pred;
//这时C为最后一个节点,设置尾节点为C
if (compareAndSetTail(pred, node)) {
//如果设置成功,设置B节点的next节点为C
pred.next = node;
return node;
}
}
//第一个阻塞的线程进入这里
//当然这里可能B和C同时进入enq方法
enq(node);
return node;
}
//这里可能很多线程一起进入。
private Node enq(final Node node) {
//for(;;)和while(true)效果是一样的,但是一般是用for(;;)因为指令少
for (;;) {
//获取尾节点
Node t = tail;
if (t == null) { // Must initialize
//这里又是CAS操作,预计head节点为空时,修改值为new Node()
//不管几个线程执行此方法,但是只有一个线程能执行成功。也就是第一个阻塞的线程
if (compareAndSetHead(new Node()))
//创建头节点成功之后,因为此时只有一个节点,所以这个节点即使头节点也是尾节点。
tail = head;
} else {
//如果只有B线程进入enq方法,第一次循环设置头节点,尾节点。
//第二次循环将线程B设置为尾节点,并且把头节点的next节点设置程B
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
自旋获取锁,没有则【 挂起当前线程】
// node为当前线程节点的前一个节点
// arg为1,表示想要修改state=1进行抢占锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的prev节点
final Node p = node.predecessor();
//如果当前节点的prev节点是head节点,并且获取锁成功
if (p == head && tryAcquire(arg)) {
// tryAcquire内调用了【setExclusiveOwnerThread方法】
//将此节点设为头节点,此时获取锁的线程为exclusiveOwnerThread
setHead(node);
p.next = null; // help GC
failed = false;
//返回是否中断
return interrupted;
}
// 线程可以被挂起 且 完成挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 中断标志设为true
interrupted = true;
}
} finally {
//如果有异常发生的话
if (failed)
//取消当前线程竞争锁,将当前node节点状态设置为cancel
cancelAcquire(node);
}
}
如果返回ture, 代表此线程中断过,那么【acquire方法】会调用【selfInterrupt方法】,此时会将当前线程中断【selfInterrupt方法】
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
如果node的pre节点不是头节点,或者node没有获取锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取当前节点的前置节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果是SIGNAL(-1)状态直接返回true,代表此节点可以挂起
//因为前置节点状态为SIGNAL,在适当状态会唤醒后继节点
return true;
if (ws > 0) {
// wx>0,cancelled。说明放弃获取锁,通过循环将这些节点从链表中剔除。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//通过CAS尝试修改pred状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
其中的node.prev = pred = pred.prev;可以看成:
当shouldParkAfterFailedAcquire返回true时,就代表允许当前线程挂起,然后就执行 parkAndCheckInterrupt()这个函数
private final boolean parkAndCheckInterrupt() {
// 挂起当前线程,线程卡在这里不再下执行,直到另一个线程unpark唤醒
// this指的是当前线程
LockSupport.park(this);
// 清理中断状态,并且返回当前的中断状态
return Thread.interrupted();
}
当前线程调用park方法后,线程进入waiting状态,需要等到另一个线程唤醒。【LockSupport.unpark(thread)】或者【thread.interrupt()】,参考代码
阻塞线程,进入等待状态(代码示例)
public static void main(String[] args) throws Exception {
Thread parkThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "进入park");
LockSupport.park(this);
System.out.println(Thread.currentThread().getName() + "解除park");
}
}, "parkThread");
parkThread.start();
}
扩展:
在正常运行任务时,调用 thread.interrupt(),经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。如果一个线程被设置中断标识后,要继续进行任务就需要被中断且清除标记位(避免线程终止)。
thread.interrupt() 和 Thread.interrupted(), 这两个就是一个线程的开关, thread.interrupt()就是将一个线程关闭,而Thread.interrupted()就是将受到thread.interrupt()作用的线程给打开阻止其关闭。
取消当前线程竞争锁,将当前node节点状态设置为cancel, 队列中移除cancel的线程节点。
找到当前节点的【前面的】第一个非cancel的节点 pred
private void cancelAcquire(Node node) {
//过滤掉无效节点
if (node == null)
return;
//当前节点线程置为空
node.thread = null;
//获取当前节点的前一个节点
Node pred = node.prev;
//跳过取消的节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//记录过滤后的节点的后置节点
Node predNext = pred.next;
//将当前节点状态改为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是tail尾节点 则将从后往前找到第一个非取消状态的节点设为tail尾节点
if (node == tail && compareAndSetTail(node, pred)) {
//如果设置成功,则tail节点后面的节点会被设置为null
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//如果当前节点不是首节点的后置节点, 并且前置节点线程不为null时
if (pred != head && pred.thread != null &&
// 如果前置节点pred 的状态是SIGNAL
// pred 状态小于0 并且设置状态为SIGNAL成功
((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))) {
//记录下当前节点的后置节点
Node next = node.next;
//如果后置节点不为空 并且后置节点的状态小于0
if (next != null && next.waitStatus <= 0){
//把当前节点的前驱节点的后继指针指向当前节点的后继节点
compareAndSetNext(pred, predNext, next);
}
} else {
//唤醒当前节点的下一个节点
unparkSuccessor(node);
}
//将当前节点下一节点指向自己
node.next = node; // help GC
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 这里是将锁的数量减1
int c = getState() - releases;
// 如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常
if (Thread.currentThread() != getExclusiveOwnerThread()){
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) {
// 由于重入的关系,不是每次释放锁c都等于0,
// 直到最后一次释放锁时,才会把当前线程释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// //判断后继节点是否为空或者是否是取消状态
if (s == null || s.waitStatus > 0) {
s = null;
//然后从队列尾部向前遍历找到最前面的一个waitStatus小于0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//获取head节点的next节点,唤醒被挂起的线程
if (s != null)
LockSupport.unpark(s.thread);
}
为什么从尾部开始向前遍历?
因为在doAcquireInterruptibly.cancelAcquire方法的处理过程中只设置了next的变化,没有设置prev的变化,在最后有这样一行代码:node.next = node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的
// true:公平锁 false:非公平锁
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
逻辑代码
lock.unlock();
构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平性:
ReentrantLock如何实现synchronized不具备的公平与非公平性呢?
ReentrantLock 内部定义了一个 Sync 的内部类,该类继承 AbstractQueuedSynchronized (AQS框架),对该抽象类的部分方法做了实现(模板模式),并且还定义了两个子类:
1、FairSync 公平锁的实现
2、NonfairSync 非公平锁的实现
如何实现公平性
- lock方法
- tryAcquire方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//将state状态从0设为1 CAS方式
if (compareAndSetState(0, 1))
//如果设定成功的话,则将当前线程(就是自己)设为占有锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
//设置失败的话,就当前线程没有抢到锁,然后进行【AQS父类】的这个方法
acquire(1);
}
// 【AQS类】的acquire调用
protected final boolean tryAcquire(int acquires) {
//调用【Sync类】非公平锁的方法
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
//获得当前线程
final Thread current = Thread.currentThread();
//获得当前锁的状态
int c = getState();
//如果锁的状态是0的话,就表明还没有线程获取到这个锁
if (c == 0) {
//进行CAS操作,将锁的状态改为acquires,因为是可重入锁,所以这个数字可能是>0的数
if (compareAndSetState(0, acquires)) {
//将当前持有锁的线程设为自己
setExclusiveOwnerThread(current);
//返回 获取锁成功
return true;
}
}// 如果当前锁的状态不是0,判断当前获取锁的线程是不是自己,如果是的话
else if (current == getExclusiveOwnerThread()) {
//则重入数加acquires (这里acquires是1) 1->2 3->4 这样
int nextc = c + acquires;
if (nextc < 0) // overflow 异常检测
throw new Error("Maximum lock count exceeded");
//将锁的状态设为当前值
setState(nextc);
//返回获取锁成功
return true;
}
//当前获取锁的线程不是自己,获取锁失败,返回
return false;
}
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 无锁,允许加锁
if (c == 0) {
// hasQueuedPredecessors 返回false,表示当前线程不用排队
// 尝试CAS修改state变量
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state不为0,且当前线程持有锁, 重复加锁。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
// 首节点h的后继节点
Node s;
// 返回false才会获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors方法的作用是判断当前线程需不需要排队,根据同步队列中是否已经有其他的线程在排队的情况来决定,如果有其他的线程在排队,就需要排队,没有,就不需要排队。此方法返回true,代表当前线程需要排队,返回false,表示当前线程不用排队。
只有返回false时,才能尝试获取锁
情况一: h != t 返回false,那么头节点和尾节点相等
情况二:h != t返回true,
(s = h.next) == null返回false以及s.thread !=Thread.currentThread()返回false
头节点(头节点是空节点)的后继节点即链表的第二个节点,也是第一个实际节点【 enq(node) 】
自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
Condition 是一个多线程协调通信的工具类,本质是一个条件队列。与Lock配合可以实现等待/通知模式,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。
任意一个Java对象,都拥有一与之关联的唯一的监视器对象monitor, 为此Java为每个对象提供了一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。
相比使用Object的wait()、notify(),使用Condition中的await()、signal()这种方式实现线程间协作更加安全和高效。AQS通过实现Condition中的方法,对外提供await(Object.wait())和signal(Object.notify())调用。
Object的监视器方法与Condition接口的对比如下(来自网络):
- Condition可以和任意的锁对象结合,监视器方法不会再绑定到某个锁对象上
- 一个锁可以对应多个条件变量, 因为一个锁对象可以多次调用newCondition方法
- lock-condition,可以实现多个条件队列,signalAll只会唤起某个条件队列下的等待线程
代码如下(示例):
/**
* ReentrantLock 实现源码学习
* @author 一枝花算不算浪漫
* @date 2020/4/28 7:20
*/
public class ReentrantLockDemo {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程一加锁成功");
System.out.println("线程一执行await被挂起");
condition.await();
System.out.println("线程一被唤醒成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程一释放锁成功");
}
}).start();
new Thread(() -> {
lock.lock();
try {
System.out.println("线程二加锁成功");
condition.signal();
System.out.println("线程二唤醒线程一");
} finally {
lock.unlock();
System.out.println("线程二释放锁成功");
}
}).start();
}
}
执行结果:
Condition内部实现原理:
注意: 线程一进入condition队列,等线程二唤醒后,再次进入等待队列,完成线程一的任务
每一个AQS对象中包含一个同步队列,类似的,每个Condition对象中都包含着一个队列(以下称为等待/条件队列),用来存放调用该Condition对象的await()方法时被阻塞的线程。该队列是Condition实现等待/通知机制的底层关键数据结构。
条件队列同样是一个FIFO的队列,结点的类型直接复用的同步队列的结点类型—AQS的静态内部类AbstractQueuedSynchronizer.Node。
条件队列工作流程
当一个获取到锁的线程调用了Condition.await()方法,那么该线程将会被构造成等待类型为Node.CONDITION的Node结点加入等待队列尾部并释放锁,加入对应Condition对象的条件队列尾部并挂起(WAITING)。
当某个线程中调用某个Condition的signal/signalAll方法,对应Condition对象的条件队列的结点会转移到锁内部的AQS对象的同步队列中,并且在获取到锁之后,对应的线程才可以继续恢复执行后续代码。
条件队列代码
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 同步队列头节点
*/
private transient volatile Node head;
/**
* 同步队列尾节点
*/
private transient volatile Node tail;
/**
* 同步状态
*/
private volatile int state;
/**
* Node节点的实现
*/
static final class Node {
//……
}
/**
* 位于AQS内部的ConditionObject类,就是Condition的实现
*/
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/**
* 条件队列头结点引用
*/
private transient Node firstWaiter;
/**
* 条件队列尾结点引用
*/
private transient Node lastWaiter;
//……
}
}
由上图可知, Condition的实现是AQS的内部类ConditionObject,因此每个Condition实例都能够访问AQS提供的方法,相当于每个Condition都拥有所属AQS的引用。
ConditionObject中持有条件队列的头结点引用firstWaiter和尾结点引用lastWaiter。。不同的是:等待队列是双向链表,条件队列是单向链表,结点之间使用nextWaiter引用维持后继的关系,并不会用到prev, next属性,它们的值都为null,并且没有哨兵结点。
如图所示,Condition拥有首尾结点的引用,而新增结点只需要将原有的尾结点nextWaiter指向它,并且更新尾结点即可。上述结点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在Object的监视器模型上,一个监视器对象只能拥有一个同步队列和等待队列,而JUC中的一个同步组件实例可以拥有一个同步队列和多个条件队列,其对应关系如下图:
/**
* 使用Condition实现有界队列
*/
public 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];
}
//添加一个元素,如果数组满了,添加线程进入等待状态,直到有“空位”
public void add(T t) {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[addIndex] = t;
if (++addIndex == items.length) {
addIndex = 0;
}
++count;
//唤醒一个等待删除的线程
notEmpty.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//由头部删除一个元素,如果数组空,则删除线程进入等待状态,知道有新元素加入
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object res = items[removeIndex];
if (++removeIndex == items.length)
removeIndex = 0;
--count;
//唤醒一个等待插入的线程
notFull.signal();
return (T) res;
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return "BoundedQueue{" +
"items=" + Arrays.toString(items) +
", addIndex=" + addIndex +
", removeIndex=" + removeIndex +
", count=" + count +
", lock=" + lock +
", notEmpty=" + notEmpty +
", notFull=" + notFull +
'}';
}
public static void main(String[] args) throws InterruptedException {
BoundedQueue<Object> objectBoundedQueue = new BoundedQueue<>(10);
for (int i = 0; i < 20; i++) {
objectBoundedQueue.add(i);
System.out.println(objectBoundedQueue);
if (i/2==0) {
objectBoundedQueue.remove();
}
}
}
}
参考文章:
1、深入分析AQS实现原理
2、【深入AQS原理】我画了35张图就是为了让你深入 AQS
3、Java的锁机制–Lock接口
4、图文并茂:AQS 是怎么运行的?
5、AQS简单介绍与使用
6、并发编程-04. AQS 与 Lock 源码详解
7、AQS详解
8、【java并发编程】ReentrantLock源码分析
9、图文并茂详解AQS加锁
10、AQS-hasQueuedPredecessors()解析
11、AQS(AbstractQueuedSynchronizer)源码深度解析(5)—条件队列的等待、通知的实现以及AQS的总结【一万字】