简单介绍
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题synchronized都是能够解决的,但是synchronized不太能满足我们的需求:
第一点:synchronized没有办法处理死锁问题,synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态之后,就嗝屁了,什么都做不了,包括释放线程已经占有的资源。所以我们希望一个锁,在已经占有部分资源的时候,在请求额外数据但是请求不到的时候主动释放锁,这么一来死锁问题就可以解决。
第二点:synchronized的控制能力没有想象中的那么强,它没有办法控制等待获取锁的线程们是否是公平的。如果在极端场合,不公平的锁机制会导致线程饥饿,线程一直都没有资源去执行。
当然synchronized虽然功能较少,但是简单又好用啊,自动就帮你释放锁了,而且随着JDK版本升级,不断优化之后性能也还不错。如果是lock机制的,还需要手动unlock,如果不小心忘了那就很可能出问题。
那么,如果我们重新设计一把互斥锁去解决上面这个问题,可以有以下方案:
- 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
- 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
恰好,JDK自带的ReentrantLock是满足我们上述需求的
Lock/Condition体系
synchronized是jvm自带的同步锁,仅仅使用一个关键字就可以实现对资源的互斥占用。但是开发者往往对此并不满足,其源自于程序员渴望控制一切的欲望。因此Lock体系诞生就诞生了,设计者期望Lock体系可以完全覆盖synchronized的功能,并且增加synchronized做不到的功能。接下来我们就来看看设计者是怎么思考这个东西的。
Lock接口
首先我们来看看设计上对顶级的Lock接口。顾名思义,Lock接口表达的是一个锁所具备的行为,对于 synchronized 的锁而言,只有加锁和解锁的行为。而设计者对Lock赋予了更多的期望,以下是Lock接口的定义。
public interface Lock {
// 加锁
void lock();
// 加锁,并支持中断
void lockInterruptibly() throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解锁
void unlock();
// 获得条件等待对象
Condition newCondition();
}
Lock 和 synchronized 是两种最常见的锁,用于控制对共享资源的访问,但是在使用上和功能上又有较大的不同。Lock提供了无条件的、可轮询的、定时的、可中断的、可多条件队列的锁操作,Lock的实现类基本都支持非公平锁和公平锁,可以说Lock是synchronized的能力升级版本。但是 Lock 并不是用来代替 synchronized 的,而是当使用 synchronized 不合适或不足以满足要求的时候,Lock 可以用来提供更高级功能的。
接下来,我们一个一个方法来看Lock接口中的方法是怎么用的。
lock()/unlock()方法:最基础的获取/释放锁的方法,可以理解为和synchronized获取/释放锁的方式一致。在线程获取锁时如果锁已被其他线程获取,则进行等待,需要进行释放锁的时候,必须由我们自己调用unlock()方法主动去释放锁,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中释放锁,以保证发生异常时锁一定被释放,如果不能保证,这段代码将会变得非常危险。逻辑上,示例代码如下所示:
Lock lock = new XxLock();
lock.lock();
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
} finally{
lock.unlock(); //释放锁
}
再继续思考,由于lock()方法不能被中断,所以可能会导致死锁,我们需要避免这种情况产生,经典的策略如下:
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
} else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
上面的代码看上去和synchronized避免死锁的策略差不多。死锁的发生是因为两个线程互相占用了对方接下去需要占用的锁资源,我们可以在一个线程获取打算获取任意一个锁的时候进行尝试动作,内层的尝试动作失败也会连带着外部的锁的资源释放,这样一来就不用担心死锁问题的产生。
lock() 方法是不能被中断,所以如果一旦陷入死锁,lock() 就会陷入永久等待。上文介绍虽然我们有策略避免死锁产生,但是我们也需要一些更兜底的策略避免死锁,因此针对加锁行为增加了 tryLock() 的策略。tryLock() 表达尝试获取锁,也就是说如果当前锁没有被其他线程占用,则获取成功并返回 true,否则表达获取锁失败并返回 false。相比于 lock(),这样的方法显然功能更强大,我们可以根据是否能获取到锁来决定后续程序的行为。因为该方法会立即返回,即便在拿不到锁时也不会一直等待,所以通常情况下,我们用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑,典型使用方法如下:
Lock lock = new XxLock();
if(lock.tryLock()) {
try{
//处理任务
}finally{
lock.unlock(); //释放锁
}
} else {
//如果不能获取锁,则做其他事情
}
根据tryLock方法的定义,很显然,tryLock与while配合的情况下就会产生和自旋锁类似的效果:
Lock lock = new XxLock();
while(lock.tryLock()) {
try{
// 处理任务
// 跳出循环
break;
} finally{
lock.unlock(); //释放锁
}
}
tryLock() 有个重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于后者会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。从方法定义上可以看到,该方法可以抛出InterruptedException异常,这表明在等待的期间,可以中断线程,从而避免死锁的发生。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
lockInterruptibly方法和lock方法的功能类似,也是主动获取锁,如果这个锁当前是可以获得的,那么这个方法会立刻返回,如果这个锁当前是不能获得的,那么当前线程便会开始等待。但是和lock()方法相比,在等待获得锁的过程中,这个方法支持被中断。如果用另一个视角来看待这个方法,你会发现lockInterruptibly()的行为等价于超时时间是无穷长的 tryLock(long time, TimeUnit unit)。
这个方法的经典用法如下:
public void lockInterruptibly() {
try {
lock.lockInterruptibly();
try {
System.out.println("操作资源");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
在这个方法中我们首先执行了 lockInterruptibly 方法,并且对它进行了 try catch 包装,然后同样假设我们能够获取到这把锁,和之前一样,就必须要使用 try-finall 来保障锁的绝对释放。
Condition接口
sychronized关键字可以挂在方法上,自动加锁解锁,lock接口就是来抄袭这个过程的,但是sychronized关键字也可以和object.wait/notify配合,达到更精确的控制。那么后者就是Condiction来控制的。可以看到Lock接口内有这样的一个方法:
Condition newCondition()
可以看出Condition获取的来源是Lock,你可以针对一把锁申请多个Condition。我们打开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();
}
可以看到,这些方法就是Object#wait与Object#notify的翻版,只不过多了一些控制的参数。我们可以简单的理解为Condition实例就是Object#await 策略中的Object本身。为了更加清晰得看到Object与Condition间的概念相似性,我们先来看看使用Object.notify机制来实现的简易版阻塞队列代码:
public class MyBlockingQueueForWaitNotify {
private int maxSize;
private LinkedList<Object> storage;
public MyBlockingQueueForWaitNotify (int size) {
this.maxSize = size;
storage = new LinkedList<>();
}
public synchronized void put() throws InterruptedException {
while (storage.size() == maxSize) {
this.wait();
}
storage.add(new Object());
this.notifyAll();
}
public synchronized void take() throws InterruptedException {
while (storage.size() == 0) {
this.wait();
}
System.out.println(storage.remove());
this.notifyAll();
}
}
那么使用Lock/Condition机制来实现的简易版阻塞队列代码会变成这个样子:
public class MyBlockingQueueForCondition {
private Queue queue;
private int max = 16;
private ReentrantLock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public MyBlockingQueueForCondition(int size) {
this.max = size;
queue = new LinkedList();
}
public void put(Object o) throws InterruptedException {
lock.lock();
try {
while (queue.size() == max) {
notFull.await();
}
queue.add(o);
notEmpty.signalAll();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (queue.size() == 0) {
notEmpty.await();
}
Object item = queue.remove();
notFull.signalAll();
return item;
} finally {
lock.unlock();
}
}
}
可以看到,两种写法特别相似。如果说 Lock 是用来代替 synchronized 的,那么 Condition 就是用来代替相对应的 Object 的 wait/notify/notifyAll,在用法和性质上几乎都一样。Condition 把 Object 的 wait/notify/notifyAll 转化为了一种相应的对象,其实现的效果基本一样,但是把更复杂的用法,变成了更直观可控的对象方法,是一种升级。await 方法会自动释放持有的 Lock 锁,和 Object 的 wait 一样,不需要自己手动释放锁。另外,调用 await 的时候必须持有锁,否则会抛出异常,这一点和 Object 的 wait 一样。
- lock.lock() 对应进入 synchronized 方法
- condition.await() 对应 object.wait()
- condition.signalAll() 对应 object.notifyAll()
- lock.unlock() 对应退出 synchronized 方法
AQS框架
synchronized 关键字的锁机制是由JVM内部实现的,程序员无法在表面上感知到加锁过程的逻辑。纯粹的Lock接口,Condition类是无法对外提供锁机制的,你会发现还缺少将他们整合在一起的逻辑。如果每次都需要开发者自行实现将会是一个灾难。因此AQS框架就诞生了,其内部已经实现了Lock与Condition的配合逻辑,只需要开发者使用很少量的代码就可以完成自定义的锁。包括JDK内自带的一些并发工具,其内部基本上都可以看到AQS的身影。AQS是一个抽象类,那么开发者使用AQS的方式是继承AQS,改写部分方法。我们将AQS内可以被覆盖的方法拿出来看看:
// 独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 独占式释放同步状态
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享式获取同步状态,返回值大于等于 0 ,则表示获取成功;否则,获取失败
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享式释放同步状态
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
要理解上面的方法,需要深刻理解AQS的设计。AQS内部最核心的设计分为三大部分,第一个是 state,它是一个数值,在不同的类中表示不同的含义。第二个是一个队列,该队列用来存放线程,这也和sychronized关键字背后存在一个队列的概念一致。第三个是"获取资源/释放资源"的相关方法,需要利用 AQS 的工具类根据自己的逻辑去实现。
第一部分:state状态
AQS源码中使用一个 int 类型的成员变量 state 来表示同步状态。
/**
* The synchronization state.
*/
private volatile int state;
state 的含义并不是一成不变的,它会根据具体实现类的作用不同而表示不同的含义,由开发者自行决定。例如以下几种情况:
- 在Semphore中,state 表达剩余许可证的数量,相当于是一个内部计数器。
- 在 CountDownLatch 中,state 表达需要“倒数”的数量,减到 0 的时候就代表这个门闩被放开。
- 在 ReentrantLock 中,state 表达锁的占有情况。state的值表达某个线程重入这个锁的深度。
于此对应的,在AQS内部提供了三个方法,针对 state 进行操作。
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
其中setState方法不会产生线程安全问题,当对基本类型的变量进行直接赋值时,如果加了 volatile 就可以保证它的线程安全。而compareAndSetState方法是我们熟知的老朋友了,使用unsafe类进行cas操作,保证原子性。
第二部分 FIFO 同步队列
当多个线程去竞争同一把锁的时候,大部分的线程事实上是抢不到的,那么那些抢不到锁的线程就需要被管理。管理的手段就是利用一个FIFO队列,将暂时抢不到锁的线程穿在一起,当前面的线程释放锁之后,AQS就会从队列中挑选一个合适的线程来尝试抢刚刚释放的那把锁。这里的FIFO队列的数据结构看似简单,但是要想维护成一个线程安全的双向队列却非常复杂,因为要考虑很多的多线程并发问题。我们来看一下 AQS 作者 Doug Lea 给出的关于这个队列的一个图示:
在队列中,分别用 head 和 tail 来表示头节点和尾节点,两者在初始化的时候都指向了一个空节点。头节点可以理解为"当前持有锁的线程",而在头节点之后的线程就被阻塞了,它们会等待被唤醒,唤醒也是由 AQS 负责操作的。
第三部分:获取/释放锁方法
获取/释放锁方法是协作工具类的逻辑具体体现,需要每一个协作工具类自己去实现,所以在不同的工具类中,它们的实现和含义各不相同。
获取方法
获取操作通常会依赖 state 变量的值,根据 state 值不同,协作工具类也会有不同的逻辑,并且在获取的时候也经常会阻塞,例如:
- ReentrantLock 中的 lock 方法就是其中一个“获取方法”,执行时,如果发现 state 不等于 0 且当前线程不是持有锁的线程,那么就代表这个锁已经被其他线程所持有了。这个时候,当然就获取不到锁,于是就让该线程进入阻塞状态。
- Semaphore 中的 acquire 方法就是其中一个“获取方法”,作用是获取许可证,此时能不能获取到这个许可证也取决于 state 的值。如果 state 值是正数,那么代表还有剩余的许可证,数量足够的话,就可以成功获取;但如果 state 是 0,则代表已经没有更多的空余许可证了,此时这个线程就获取不到许可证,会进入阻塞状态,所以这里同样也是和 state 的值相关的。
- CountDownLatch 获取方法就是 await 方法(包含重载方法),作用是“等待,直到倒数结束”。执行 await 的时候会判断 state 的值,如果 state 不等于 0,线程就陷入阻塞状态,直到其他线程执行倒数方法把 state 减为 0,此时就代表现在这个门闩放开了,所以之前阻塞的线程就会被唤醒。
获取方法在不同的类中代表不同的含义,但往往和 state 值相关,也经常会让线程进入阻塞状态,这也同样证明了 state 状态在 AQS 类中的重要地位。
释放方法
释放方法是获取方法的对立面,和刚才的获取方法配合使用。我们刚才讲的获取方法可能会让线程阻塞,比如说获取不到锁就会让线程进入阻塞状态,但是释放方法通常是不会阻塞线程的。例如:
- Semaphore 信号量里面,释放就是 release 方法(包含重载方法),release() 方法的作用是去释放一个许可证,会让 state 加 1
- CountDownLatch 里面,释放就是 countDown 方法,作用是倒数一个数,让 state 减 1。
AQS核心介绍
ReentantLock很重要的一块儿就是AQS框架,因此我们先来看看AQS大致是怎么工作的。
核心字段
// 队列的头节点
private transient volatile Node head;
// 队列的尾节点
private transient volatile Node tail;
// 控制加锁解锁的状态变量
private volatile int state;
AQS内定义了一个状态变量和head/tail结点(构成链表),状态变量用来控制加锁解锁,队列用来放置等待的线程。这几个变量都要使用volatile关键字来修饰,因为是在多线程环境下操作,要保证它们的值修改之后对其它线程立即可见。这几个变量的修改是直接使用的Unsafe这个类操作的:
// 获取Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 状态变量state的偏移量
private static final long stateOffset;
// 头节点的偏移量
private static final long headOffset;
// 尾节点的偏移量
private static final long tailOffset;
// 等待状态的偏移量(Node的属性)
private static final long waitStatusOffset;
// 下一个节点的偏移量(Node的属性)
private static final long nextOffset;
static {
try {
// 获取state的偏移量
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
// 获取head的偏移量
headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
// 获取tail的偏移量
tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
// 获取waitStatus的偏移量
waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
// 获取next的偏移量
nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
} catch (Exception ex) {
throw new Error(ex);
}
}
// 调用Unsafe的方法原子更新state
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
核心内部类
这里核心内部类总共有两个,一个是结点的标识,一个是条件的表示,这里先说下结点,后面再展开Condition。
static final class Node {
// 标识一个节点是共享模式
static final Node SHARED = new Node();
// 标识一个节点是互斥模式
static final Node EXCLUSIVE = null;
// 标识线程已取消
static final int CANCELLED = 1;
// 标识后继节点需要唤醒
static final int SIGNAL = -1;
// 标识线程等待在一个条件上
static final int CONDITION = -2;
// 标识后面的共享锁需要无条件的传播(共享锁需要连续唤醒读的线程)
static final int PROPAGATE = -3;
// 当前节点保存的线程对应的等待状态
volatile int waitStatus;
// 前一个节点
volatile Node prev;
// 后一个节点
volatile Node next;
// 当前节点保存的线程
volatile Thread thread;
// 下一个等待在条件上的节点(Condition锁时使用)
Node nextWaiter;
// 是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取前一个节点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 节点的构造方法
Node() {}
// 节点的构造方法
Node(Thread thread, Node mode) {
// 把共享模式还是互斥模式存储到nextWaiter这个字段里面了
this.nextWaiter = mode;
this.thread = thread;
}
// 节点的构造方法
Node(Thread thread, int waitStatus) {
// 等待的状态,在Condition中使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
waitStatus
字段,等待状态,用来控制线程的阻塞和唤醒,并且可以避免不必要的调用LockSupport的#park(...)
和#unpark(...)
方法。。目前有 4 种:CANCELLED
SIGNAL
CONDITION
PROPAGATE
。 实际上,有第 5 种,INITAL
,值为 0 ,初始状态。每个等待状态代表的含义,它不仅仅指的是 Node 自己的线程的等待状态,也可以是下一个节点的线程的等待状态。- CLH 同步队列:
prev
和next
字段,是 AbstractQueuedSynchronizer 的字段,分别指向同步队列的头和尾。head
和tail
字段,分别指向 Node 节点的前一个和后一个 Node 节点,从而实现链式双向队列。再配合上prev
和next
字段,快速定位到同步队列的头尾。
thread
字段,Node 节点对应的线程 Thread 。nextWaiter
字段,Node 节点获取同步状态的模型( Mode )。#tryAcquire(int args)
和#tryAcquireShared(int args)
方法,分别是独占式和共享式获取同步状态。在获取失败时,它们都会调用#addWaiter(Node mode)
方法入队。而nextWaiter
就是用来表示是哪种模式:SHARED
静态 + 不可变字段,枚举共享模式。EXCLUSIVE
静态 + 不可变字段,枚举独占模式。#isShared()
方法,判断是否为共享式获取同步状态。
#predecessor()
方法,获得 Node 节点的前一个 Node 节点。在方法的内部,Node p = prev
的本地拷贝,是为了避免并发情况下,prev
判断完== null
时,恰好被修改,从而保证线程安全。- 构造方法有 3 个,分别是:
#Node()
方法:用于SHARED
的创建。- #Node(Thread thread, Node mode) 方法
- #Node(Thread thread, int waitStatus)
抽象方法
可以看到AQS是个abstract的类,意味着子类必须实现它的方法。AQS内待实现方法如下:
// 互斥模式下使用:尝试获取锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 互斥模式下使用:尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试获取锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试释放锁
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 如果当前线程独占着锁,返回true
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
可以看到都是一些默认实现,原因是当我们需要实现一个锁的时候可能只需要实现其中一部分即可。
如果想使用 AQS 来写一个自己的线程协作工具类,通常而言是分为以下三步,这也是 JDK 里利用 AQS 类的主要步骤。
1)新建一个自己的线程协作工具类,在内部写一个 Sync 类,该 Sync 类继承 AbstractQueuedSynchronizer。
2)想好设计的线程协作工具类的协作逻辑,在 Sync 类里,根据是否是独占,来重写对应的方法。如果是独占,则重写 tryAcquire 和 tryRelease 等方法;如果是非独占,则重写 tryAcquireShared 和 tryReleaseShared 等方法。
3)在自己的线程协作工具类中,实现获取/释放的相关方法,并在里面调用 AQS 对应的方法,如果是独占则调用 acquire 或 release 等方法,非独占则调用 acquireShared 或 releaseShared 或 acquireSharedInterruptibly 等方法。
接下来我们先使用AQS造一个我们自己的锁,看看AQS是怎么玩儿的:
public class MyLockBaseOnAqs {
// 定义一个同步器,实现AQS类
private static class Sync extends AbstractQueuedSynchronizer {
// 实现tryAcquire(acquires)方法
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 实现tryRelease(releases)方法
@Override
protected boolean tryRelease(int releases) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
// 声明同步器
private final Sync sync = new Sync();
// 加锁
public void lock() {
sync.acquire(1);
}
// 解锁
public void unlock() {
sync.release(1);
}
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
MyLockBaseOnAqs lock = new MyLockBaseOnAqs();
CountDownLatch countDownLatch = new CountDownLatch(1000);
IntStream.range(0, 1000).forEach(i -> new Thread(() -> {
lock.lock();
try {
IntStream.range(0, 10000).forEach(j -> {
count++;
});
} finally {
lock.unlock();
}
countDownLatch.countDown();
}, "tt-" + i).start());
countDownLatch.await();
System.out.println(count);
}
}
通过结果我们可以看出,这个锁是可以正常工作的。只要简单的套用一下,我们就可以造出符合自己预期的锁工具。
公平锁/非公平锁
接下来我们就正式开始看看ReentrantLock是怎么实现的
构造方法
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
// 默认构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
// 自己可选择使用公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock内部存在sync的字段,抽象类Sync实现了AQS的部分方法,NonfairSync实现了Sync,主要用于非公平锁的获取,FairSync也实现了Sync,用于公平锁的获取。在初始化的时候就是对sync的初始化。ReentrantLock有两个构造器,默认会将sync赋值为非公平锁。
重要方法
lock方法
这里我们先分析公平锁是怎么回事。非公平锁和公平锁仅有小部分区别。
// ReentrantLock.lock()
public void lock() {
// 调用的sync属性的lock()方法
// 这里的sync是公平锁,所以是FairSync的实例
sync.lock();
}
// ReentrantLock.FairSync.lock()
final void lock() {
// 调用AQS的acquire()方法获取锁
// 这里传的值为1
acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
// 尝试获取锁
// 如果失败了,就排队
if (!tryAcquire(arg) &&
// addWaiter()这里传入的节点模式为独占模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 查看当前状态变量的值
int c = getState();
// 如果状态变量的值为0,说明暂时还没有人占有锁
if (c == 0) {
// 如果没有其它线程在排队,那么当前线程尝试更新state的值为1
// 如果成功了,则说明当前线程获取了锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中
// exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量
setExclusiveOwnerThread(current);
// 返回true说明成功获取了锁
return true;
}
}
// 如果当前线程本身就占有着锁,现在又尝试获取锁
// 那么,直接让它获取锁并返回true
else if (current == getExclusiveOwnerThread()) {
// 状态变量state的值加1
int nextc = c + acquires;
// 如果溢出了,则报错
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置到state中
// 这里不需要CAS更新state
// 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
// 所以不存在竞争,自然不需要使用CAS来更新
setState(nextc);
// 当线程获取锁成功
return true;
}
// 当前线程尝试获取锁失败
return false;
}
// AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面尝试获取锁失败了
private Node addWaiter(Node mode) {
// 新建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 这里先尝试把新节点加到尾节点后面
// 如果成功了就返回新节点
// 如果没成功再调用enq()方法不断尝试
Node pred = tail;
// 如果尾节点不为空
if (pred != null) {
// 设置新节点的前置节点为现在的尾节点
node.prev = pred;
// CAS更新尾节点为新节点
if (compareAndSetTail(pred, node)) {
// 如果成功了,把旧尾节点的下一个节点指向新节点
pred.next = node;
// 并返回新节点
return node;
}
}
// 如果上面尝试入队新节点没成功,调用enq()处理
enq(node);
return node;
}
// AbstractQueuedSynchronizer.enq()
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;
// CAS更新尾节点为新节点
if (compareAndSetTail(t, node)) {
// 成功了,则设置旧尾节点的下一个节点为新节点
t.next = node;
// 并返回旧尾节点
return t;
}
}
}
}
// AbstractQueuedSynchronizer.acquireQueued()
// 调用上面的addWaiter()方法使得新节点已经成功入队了
// 这个方法是尝试让当前节点来获取锁的
final boolean acquireQueued(final Node node, int arg) {
// 失败标记
boolean failed = true;
try {
// 中断标记
boolean interrupted = false;
// 自旋
for (;;) {
// 当前节点的前一个节点
final Node p = node.predecessor();
// 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了
// 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 尝试获取锁成功
// 这里同时只会有一个线程在执行,所以不需要用CAS更新
// 把当前节点设置为新的头节点
setHead(node);
// 并把上一个节点从链表中删除
p.next = null; // help GC
// 未失败
failed = false;
return interrupted;
}
// 是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
// 真正阻塞的方法
parkAndCheckInterrupt())
// 如果中断了
interrupted = true;
}
} finally {
// 如果失败了
if (failed)
// 取消获取锁
cancelAcquire(node);
}
}
// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
// 这个方法是在上面的for()循环里面调用的
// 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false
// 第二次调用才会返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 上一个节点的等待状态
// 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定
// 也就是说使用的是默认值0
// 这里把各种等待状态再贴出来
//static final int CANCELLED = 1;
//static final int SIGNAL = -1;
//static final int CONDITION = -2;
//static final int PROPAGATE = -3;
int ws = pred.waitStatus;
// 如果等待状态为SIGNAL(等待唤醒),直接返回true
if (ws == Node.SIGNAL)
return true;
// 如果前一个节点的状态大于0,也就是已取消状态
if (ws > 0) {
// 把前面所有取消状态的节点都从链表中删除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒
// 这里可以简单地理解为把初始状态0设置为SIGNAL
// CONDITION是条件锁的时候使用的
// PROPAGATE是共享锁使用的
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// AbstractQueuedSynchronizer.parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
// 底层调用的是Unsafe的park()方法
LockSupport.park(this);
// 返回是否已中断
return Thread.interrupted();
}
下面我们看一下主要方法的调用关系:
ReentrantLock#lock()
->ReentrantLock.FairSync#lock() // 公平模式获取锁
->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法
->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁
->AbstractQueuedSynchronizer#addWaiter() // 添加到队列
->AbstractQueuedSynchronizer#enq() // 入队
->AbstractQueuedSynchronizer#acquireQueued() // 里面有个for()循环,唤醒后再次尝试获取锁
->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查是否要阻塞
->AbstractQueuedSynchronizer#parkAndCheckInterrupt() // 真正阻塞的地方
获取锁的主要过程大致如下:
(1)尝试获取锁,如果获取到了就直接返回了;
(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;
(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;
(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);
(5)调用parkAndCheckInterrupt()阻塞当前线程;
(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;
(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。
以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。
// ReentrantLock.lock()
public void lock() {
sync.lock();
}
// ReentrantLock.NonfairSync.lock()
// 这个方法在公平锁模式下是直接调用的acquire(1);
final void lock() {
// 直接尝试CAS更新状态变量
if (compareAndSetState(0, 1))
// 如果更新成功,说明获取到锁,把当前线程设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// ReentrantLock.NonfairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
// 调用父类的方法
return nonfairTryAcquire(acquires);
}
// ReentrantLock.Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 如果状态变量的值为0,再次尝试CAS更新状态变量的值
// 相对于公平锁模式少了!hasQueuedPredecessors()条件
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;
}
相对于公平锁,非公平锁加锁的过程主要有两点不同:
(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;
(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队呢;
总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程。
lockInterruptibly方法
支持线程中断,它与lock()方法的主要区别在于lockInterruptibly()获取锁的时候如果线程中断了,会抛出一个异常,而lock()不会管线程是否中断都会一直尝试获取锁,获取锁之后把自己标记为已中断,继续执行自己的逻辑,后面也会正常释放锁。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
和上面的lock相比,逻辑是差不都的,就是增加了对中断的处理。
tryLock
尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。
// ReentrantLock.tryLock()
public boolean tryLock() {
// 直接调用Sync的nonfairTryAcquire()方法
return sync.nonfairTryAcquire(1);
}
// ReentrantLock.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;
}
}
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;
}
tryLock()方法比较简单,直接以非公平的模式去尝试获取一次锁,获取到了或者锁本来就是当前线程占有着就返回true,否则返回false。
tryLock(long time, TimeUnit unit)方法
尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false。
// ReentrantLock.tryLock()
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
// 调用AQS中的方法
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// AbstractQueuedSynchronizer.tryAcquireNanos()
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果线程中断了,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 先尝试获取一次锁
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
// AbstractQueuedSynchronizer.doAcquireNanos()
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果时间已经到期了,直接返回false
if (nanosTimeout <= 0L)
return false;
// 到期时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
// 如果到期了,就直接返回false
if (nanosTimeout <= 0L)
return false;
// spinForTimeoutThreshold = 1000L;
// 只有到期时间大于1000纳秒,才阻塞
// 小于等于1000纳秒,直接自旋解决就得了
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞一段时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryLock(long time, TimeUnit unit)方法在阻塞的时候加上阻塞时间,并且会随时检查是否到期,只要到期了没获取到锁就返回false。
unLock方法
// java.util.concurrent.locks.ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer.release
public final boolean release(int arg) {
// 调用AQS实现类的tryRelease()方法释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果头节点不为空,且等待状态不是0,就唤醒下一个节点
// 还记得waitStatus吗?
// 在每个节点阻塞之前会把其上一个节点的等待状态设为SIGNAL(-1)
// 所以,SIGNAL的准确理解应该是唤醒下一个等待的线程
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;
// 如果当前线程不是占有着锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果状态变量的值为0了,说明完全释放了锁
// 这也就是为什么重入锁调用了多少次lock()就要调用多少次unlock()的原因
// 如果不这样做,会导致锁不会完全释放,别的线程永远无法获取到锁
if (c == 0) {
free = true;
// 清空占有线程
setExclusiveOwnerThread(null);
}
// 设置状态变量的值
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
// 注意,这里的node是头节点
// 如果头节点的等待状态小于0,就把它设置为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 头节点的下一个节点
Node s = node.next;
// 如果下一个节点为空,或者其等待状态大于0(实际为已取消)
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点向前遍历取到队列最前面的那个状态不是已取消状态的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果下一个节点不为空,则唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
释放锁的过程大致是,先将state的值减1,如果state减到了0,说明已经完全释放锁了,唤醒下一个等待着的节点。
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
- 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
- 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。
条件锁
条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。比如,在阻塞队列中,当队列中没有元素的时候是无法弹出一个元素的,这时候就需要阻塞在条件notEmpty上,等待其它线程往里面放入一个元素后,唤醒这个条件notEmpty,当前线程才可以继续去做“弹出一个元素”的行为。这里的条件,必须是在获取锁之后去等待,对应到ReentrantLock的条件锁,就是获取锁之后才能调用condition.await()方法。在java中,条件锁的实现都在AQS的ConditionObject类中,ConditionObject实现了Condition接口。
条件锁使用案例
这里先写个例子,看看条件锁是怎么用的。
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
// 声明一个重入锁
ReentrantLock lock = new ReentrantLock();
// 声明一个条件锁
Condition condition = lock.newCondition();
new Thread(()->{
try {
lock.lock();
try {
System.out.println("before await");
// 等待条件
condition.await();
System.out.println("after await");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 这里睡1000ms是为了让上面的线程先获取到锁
Thread.sleep(1000);
lock.lock();
try {
// 这里睡2000ms代表这个线程执行业务需要的时间
Thread.sleep(2000);
System.out.println("before signal");
// 通知条件已成立
condition.signal();
System.out.println("after signal");
} finally {
lock.unlock();
}
}
}
看代码是比较好理解的,第一个线程锁住lock之后立刻对条件进行await,等待被唤醒,另一个线程在获得锁后唤醒,释放锁后,第一个线程可以顺利获得锁并继续执行。
重要属性
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
可以看到条件锁中也维护了一个队列,为了和AQS的队列区分,我这里称为条件队列,firstWaiter是队列的头节点,lastWaiter是队列的尾节点。
构造器
// ReentrantLock.newCondition()
public Condition newCondition() {
return sync.newCondition();
}
// ReentrantLock.Sync.newCondition()
final ConditionObject newCondition() {
return new ConditionObject();
}
// AbstractQueuedSynchronizer.ConditionObject.ConditionObject()
public ConditionObject() { }
新建一个条件锁最后就是调用的AQS中的ConditionObject类来实例化条件锁。
重要方法
condition.await()方法
condition.await()方法,表明现在要等待条件的出现。
// AbstractQueuedSynchronizer.ConditionObject.await()
public final void await() throws InterruptedException {
// 如果线程中断了,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 添加节点到Condition的队列中,并返回该节点
Node node = addConditionWaiter();
// 完全释放当前线程获取的锁
// 因为锁是可重入的,所以这里要把获取的锁全部释放
int savedState = fullyRelease(node);
int interruptMode = 0;
// 是否在同步队列中
while (!isOnSyncQueue(node)) {
// 阻塞当前线程
LockSupport.park(this);
// 直到这里,调用await()时释放自己占有的锁,并阻塞自己等待条件的出现
// 当条件满足的时候会signal 执行unpark,接下去就是尝试获得锁
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 尝试获取锁,注意第二个参数,这是上一章分析过的方法
// 如果没获取到会再次阻塞(这个方法这里就不贴出来了,有兴趣的翻翻上一章的内容)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除取消的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 线程中断相关
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// AbstractQueuedSynchronizer.ConditionObject.addConditionWaiter
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果条件队列的尾节点已取消,从头节点开始清除所有已取消的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
// 重新获取尾节点
t = lastWaiter;
}
// 新建一个节点,它的等待状态是CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果尾节点为空,则把新节点赋值给头节点(相当于初始化队列)
// 否则把新节点赋值给尾节点的nextWaiter指针
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
// 尾节点指向新节点
lastWaiter = node;
// 返回新节点
return node;
}
// AbstractQueuedSynchronizer.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;
}
}
// AbstractQueuedSynchronizer.isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
// 如果等待状态是CONDITION,或者前一个指针为空,返回false
// 说明还没有移到AQS的队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果next指针有值,说明已经移到AQS的队列中了
if (node.next != null) // If has successor, it must be on queue
return true;
// 从AQS的尾节点开始往前寻找看是否可以找到当前节点,找到了也说明已经在AQS的队列中了
return findNodeFromTail(node);
}
对比Aqs,Condition有以下不太一样的东西:
1)Condition的队列和AQS的队列不完全一样,AQS的队列头节点是不存在任何值的,是一个虚节点;Condition的队列头节点是存储着实实在在的元素值的,是真实节点。
2)各种等待状态(waitStatus)的变化:
- 在条件队列中,新建节点的初始等待状态是CONDITION(-2);
- 移到AQS的队列中时等待状态会更改为0(AQS队列节点的初始等待状态为0);
- 在AQS的队列中如果需要阻塞,会把它上一个节点的等待状态设置为SIGNAL(-1);
- 不管在Condition队列还是AQS队列中,已取消的节点的等待状态都会设置为CANCELLED(1);
- 后面我们在共享锁的时候还会讲到另外一种等待状态叫PROPAGATE(-3)。
3)前后节点命名不一样:AQS中下一个节点是next,上一个节点是prev;Condition中下一个节点是nextWaiter,没有上一个节点。
condition.signal()方法
condition.signal()方法通知条件已经出现。
// AbstractQueuedSynchronizer.ConditionObject.signal
public final void signal() {
// 如果不是当前线程占有着锁,调用这个方法抛出异常
// 说明signal()也要在获取锁之后执行
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 条件队列的头节点
Node first = firstWaiter;
// 如果有等待条件的节点,则通知它条件已成立
if (first != null)
doSignal(first);
}
// AbstractQueuedSynchronizer.ConditionObject.doSignal
private void doSignal(Node first) {
do {
// 移到条件队列的头节点往后一位
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 相当于把头节点从队列中出队
first.nextWaiter = null;
// 转移节点到AQS队列中
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// AbstractQueuedSynchronizer.transferForSignal
final boolean transferForSignal(Node node) {
// 把节点的状态更改为0,也就是说即将移到AQS队列中
// 如果失败了,说明节点已经被改成取消状态了
// 返回false,通过上面的循环可知会寻找下一个可用节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 调用AQS的入队方法把节点移到AQS的队列中
// 注意,这里enq()的返回值是node的上一个节点,也就是旧尾节点
Node p = enq(node);
// 上一个节点的等待状态
int ws = p.waitStatus;
// 如果上一个节点已取消了,或者更新状态为SIGNAL失败(也是说明上一个节点已经取消了)
// 则直接唤醒当前节点对应的线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 如果更新上一个节点的等待状态为SIGNAL成功了
// 则返回true,这时上面的循环不成立了,退出循环,也就是只通知了一个节点
// 此时当前节点还是阻塞状态
// 也就是说调用signal()的时候并不会真正唤醒一个节点
// 只是把节点从条件队列移到AQS队列中
return true;
}
signal()方法的大致流程为:
(1)从条件队列的头节点开始寻找一个非取消状态的节点;
(2)把它从条件队列移到AQS队列;
(3)且只移动一个节点;
注意,这里调用signal()方法后并不会真正唤醒一个节点,而是最终执行lock.unlock()方法,此时才会真正唤醒一个节点,唤醒的这个节点如果曾经是条件节点的话又会继续执行await()方法park之后的方法。
总结
我们这会聊了ReentrantLock,在实现上底层使用LockSupport来进行线程阻塞相关的行为。整体功能上看是可以和sycnized关键字对标的,并且支持了一些synchronized不支持的能力。ReentrantLock内存在AQS框架,在JDK内大量同步工具在使用。