https://javadoop.com/post/AbstractQueuedSynchronizer
自己画的流程图:https://www.processon.com/view/link/5ede205d5653bb6963dc6d56
一、AQS前情
在JAVA中,sun.misc.Unsafe
类提供了硬件级别的原子操作来实现这个CAS。 java.util.concurrent
包下的大量类都使用了这个 Unsafe.java
类的CAS操作。
CAS:
java.util.concurrent.atomic
包下的类大多是使用CAS操作来实现的(如 AtomicInteger.java
,AtomicBoolean
,AtomicLong
)。下面以 AtomicInteger.java
的部分实现来大致讲解下这些原子类的实现。
一般来说在竞争不是特别激烈的时候,使用该包下的原子操作性能比使用 synchronized 关键字的方式高效的多(查看getAndSet(),可知如果资源竞争十分激烈的话,这个for循环可能换持续很久都不能成功跳出。不过这种情况可能需要考虑降低资源竞争才是)。
在较多的场景我们都可能会使用到这些原子类操作。一个典型应用就是计数了,在多线程的情况下需要考虑线程安全问题。通常第一映像可能就是:
public class Counter {//普通
private int count;
public Counter(){}
public int getCount(){
return count;
}
public void increase(){
count++;
}
}
上面这个类在多线程环境下会有线程安全问题,要解决这个问题最简单的方式可能就是通过加锁的方式,调整如下:
public class Counter {
private int count;
public Counter(){}
// 加synchronized
public synchronized int getCount(){
return count;
}
public synchronized void increase(){
count++;
}
}
这类似于悲观锁的实现,我需要获取这个资源,那么我就给他加锁,别的线程都无法访问该资源,直到我操作完后释放对该资源的锁。我们知道,悲观锁的效率是不如乐观锁的,上面说了Atomic下的原子类的实现是类似乐观锁的,效率会比使用 synchronized关键字高,推荐使用这种方式,实现如下:
public class Counter {
private AtomicInteger count = new AtomicInteger();
public Counter(){}
public int getCount(){
return count.get();
}
public void increase(){
count.getAndIncrement();
}
}
但下面的自旋也许能给你提供另一种思路
自旋实现同步
volatile int status=0;//标识--是否有线程再同步块---是否有线程上锁成功
void lock(){
while(!compareAndSet(0,1)){ // cas是线程安全的
//本来希望的oldvalue是0,但是被其他线程修改为了1,所以当前线程就一个劲等,等到他被其他线程是否了后其他线程修改为0,当前线程就能拿到锁跳出循环了
}
//加锁成功
}
void unlock(){
status=0;
}
boolean compareAndSet(int except,int newValue){
//cas操作,修改status成功则返回true
}
缺点:耗费cpu资源,没有竞争到的线程会一直占用cpu资源进行cas操作
解决思路:让得不到锁的线程让出cpu
yield+自旋
volatile int status=0;
void lock(){
while(!compareAndSet(0,1)){
yield();//自己实现,拿不到锁就释放cpu,等其他线程唤醒
}
//拿锁成功
}
void unlock(){
status=0;
}
要解决自旋锁的性能问题必须让竞争锁失败的线程不空转,而是在获取不到锁的时候把cpu资源让出来,yield方法能让出cpu资源,让线程竞争锁失败时,会调用yield方法让出cpu,自旋+yield的方式并没有完全解决问题,当系统只有两个线程竞争锁时,yield是由有效的,需要注意的是改方法只是当前让出cpu,有可能操作系统下次还是选择运行该线程,比如里面有2000个线程,想想会有什么问题?
sleep+自旋
volatile int status=0;
void lock(){
while(!compareAndSet(0,1)){
sleep(10);//时间设为固定,也很有问题
}
//拿锁成功
}
void unlock(){
status=0;
}
park+自旋
这正是AQS的核心思路,park也是挂起线程,释放cpu,等其他线程唤醒
这个LockSupport.park()
和LockSupport.unpark(线程)
是关键
volatile int status=0;
Queue parkQueue;//集合 数组 list
void lock(){
while(!compareAndSet(0,1)){
park();//入队线程,释放cpu,等待其他线程唤醒
}
//拿锁成功
...;
unlock();// status=0;
}
void unlock(){
status=0;
}
void park(){
//将当前线程加入等待队列
parkQueue.add(currentThread);
//将当前线程是否cpu 阻塞
releaseCpu();
}
void lock_notify(){
//得到要唤醒的线程头部线程
Thread t = parkQueue.header();
//唤醒等待线程
unpark(t);
}
park unpark
https://www.cnblogs.com/takumicx/p/9328459.html
park的意思是停车,等待别人告诉可以开车再启动发动机unpark,一般是利用LockSupport类完成的,而他里面的park和unpart是native的
我们可以使用他来阻塞和唤醒线程,功能和wait、notify有些相似,但是LockSupport比起wait、notify功能更强大。
LockSupport.park();//阻塞当前线程 // 该线程信息已经提前入队
// 出队后唤醒
LockSupport.unpark(Thread t);//唤醒指定线程
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("开始线程唤醒3");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒4");
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("开始线程阻塞1");
LockSupport.park();
System.out.println("结束线程阻塞2");
}
}
}
//1324
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
System.out.println("开始线程唤醒3");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒4");
}
static class ParkThread implements Runnable{
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始线程阻塞1");
LockSupport.park();
System.out.println("结束线程阻塞2");
}
}
}
// 3412
public class LockSupportTest {
public static void main(String[] args) {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
for(int i=0;i<2;i++){
System.out.println("开始线程唤醒3");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒4");
}
}
static class ParkThread implements Runnable{
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
System.out.println("开始线程阻塞1");
LockSupport.park();
System.out.println("结束线程阻塞2");
}
}
}
}
// 输出顺序
// 3434121
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
LockSupport
LockSupport就是通过控制变量_counter
来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。
- 当调用
park()
方法时,会将_counter置为0,同时判断前值,小于1说明前面被unpark
过,则直接退出,否则将使该线程阻塞。 - 当调用
unpark()
方法时,会将_counter置为1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。 - 为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。 - 为什么唤醒两次后阻塞两次会阻塞线程。
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。
LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。
二、AQS
概念
所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。是除了java自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks包。
AQS是Java并发包提供的一个同步基础机制,是并发包中实现Lock和其他同步机制(如:Semaphore、CountDownLatch和FutureTask等)的基础。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。
- 同步队列:AQS内部包含一个FIFO的同步等待队列,简单的说,没有成功获取控制权的线程会在这个队列中等待。
- state:AQS内部管理了一个原子的int域作为内部状态信息,并提供了一些方法来访问该域,基于AQS实现的同步机制可以按自己的需要来灵活使用这个int域,比如:
- ReentrantLock:用它记录锁重入次数;
- CountDownLatch:用它表示内部的count;
- FutureTask:用它表示任务运行状态(Running,Ran和Cancelled);
- Semaphore:用它表示信号数量。
- volatile修饰,代表可见性
- AQS内部提供了一个ConditionObject类来支持独占模式下的(锁)条件,这个条件的功能与Object的wait和notify/notifyAll的功能类似,但更加明确和易用。
- AQS一般的使用方式为定义一个实现AQS接口的非公有的内部帮助类作为内部代理,来实现具体同步机制的方法,如Lock的lock和unlock;AQS中也提供一些检测和监控内部队列和条件对象的方法,具体同步机制可以按需使用这些方法;AQS内部只有一个状态,即原子int域,如果基于AQS实现的类需要做序列化/反序列化,注意这一点。
// 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
private transient volatile Node tail;
// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronize
AQS的核心思想是,
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,
- 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
- CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
- AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
- 用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变state状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
- 头结点是当前获取到锁的那个线程
同步控制对比:
- synchronized:基于JVM底层,基于C++,底层行为不可控
- AbstractQueueSynchronizer:不利于任何jvm内置锁,基于java可变行为实现同步
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
三、AQS–队列
AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,这个队列是用双线链表实现的
- 如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node,放入到同步队列尾部,作为尾结点,同时再阻塞该线程。
- 当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。只需重新设置新的队列首部(头节点)即可。
添加节点
当出现锁竞争以及释放锁的时候,AQS同步队列中的节点会发生变化,首先看一下添加节点的场景。
这里会涉及到两个变化
- 新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
- 通过CAS将tail重新指向新的尾部节点
释放锁 移除节点
head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点;如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下
这个过程也是涉及到两个变化
- 修改head节点指向下一个获得锁的节点
- 新的获得锁的节点,将prev的指针指向null
这里有一个小的变化,就是设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要CAS保证,只需要把head节点设置为原首节点的后继节点,并且断开原head节点的next引用即可
看一下Node结点类
四、Node结点
等待队列中每个线程被包装成一个 Node 实例,数据结构是链表,一起看看源码吧:
static final class Node {
Node() {} // 用于创建初始头结点或者共享标志
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread; // 标识结点所代表的线程,后续用于唤醒
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
结点状态
/** 表示节点当前在共享模式下 */
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;
/** 表示接下来的一个共享模式请求(acquireShared)要无条件的传递(往后继节点方向)下去 */
static final int PROPAGATE = -3;
/**
* 等待状态域, 取以下值:
* SIGNAL: 当前节点的后继节点已经(或即将)被阻塞(在等待),所以如果当前节点释放(控制权)
* 或者被取消时,必须唤醒其后继节点运行后继。为了避免竞争,请求方法必须首先
* 声明它们需要一个信号,然后(原子的)调用请求方法,如果失败,当前线程
* 进入阻塞状态。
* CANCELLED: 表示当前节点已经被取消(由于等待超时或被中断),从队列中移走。节点一旦进入被取消状态,就
* 不会再变成其他状态了。具体来说,一个被取消节点的线程永远不会再次被
* 阻塞
* CONDITION: 表示当前节点正处在一个条件队列中。当前节点直到转移时才会被作为一个
* 同步队列的节点使用。转移时状态域会被设置为0。(使用0值和其他定义值
* 并没有关系,只是为了简化操作)
* PROPAGATE: 表示一个共享的释放操作(releaseShared)应该被传递到其他节点。该状态
* 值在doReleaseShared过程中进行设置(仅在头节点),从而保证持续传递,
* 即使其他操作已经开始。 共享,表示状态要往后面传播
* 0: None of the above 初始状态
*
* 这些状态值之所以用数值来表示,目的是为了方便使用,非负的值意味着节点不需要信号(被唤醒)。
* 所以,一些代码中不需要针对特殊值去做检测,只需要检查符号(正负)即可。
*
* 针对普通的同步节点,这个域被初始化为0;针对条件(condition)节点,初始化为CONDITION(-2)
* 需要通过CAS操作来修改这个域(如果可能的话,可以使用volatile写操作)。
*/
// 暂时只需要知道如果这个值 大于0 代表此线程取消了等待,
volatile int waitStatus;
前后指针
双向链表,有pre和next指针。
- next指针不用说肯定是找到下个该执行的任务
- pre指针用于新入队结点时从尾到头去掉取消的结点
/**
* 指向当前节点的前驱节点,用于检测等待状态。这个域在入队时赋值,出队时置空。
* 而且,在取消前驱节点的过程中,可以缩短寻找非取消状态节点的过程。由于头节点
* 永远不会取消(一个节点只有请求成功才会变成头节点,一个被取消的节点永远不可
* 能请求成功,而且一个线程只能取消自己所在的节点),所以总是存在一个非取消状态节点。
*/
volatile Node prev;
/**
* 指向当前节点的后继节点,释放(控制权)时会唤醒该节点。这个域在入队时赋值,在跳过
* 取消状态节点时进行调整,在出队时置空。入队操作在完成之前并不会对一个前驱节点的
* next域赋值,所以一个节点的next域为null并不能说明这个节点在队列尾部。然而,如果
* next域为null,我们可以从尾节点通过前驱节点往前扫描来做双重检测。取消状态节点的
* next域指向自身,这样可以简化isOnSyncQueue的实现。
*/
volatile Node next;
/**
* 指向下一个条件等待状态节点或者为特殊值(SHARED)。由于条件队列只有在独占模式下才
* 能访问,所以我们只需要一个普通的链表队列来保存处于等待状态的节点。它们在重新请
* 求的时候会转移到同步队列。由于条件只存在于独占模式下,所以如果是共享模式,就将
* 这域保存为一个特殊值(SHARED)。
*/
Node nextWaiter;
/**
* 使当前节点入队的线程。在构造构造的时候初始化,使用后置为null。
*/
volatile Thread thread;
/** 判断是否在共享锁状态 */
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
说明:节点类Node内部定义了一些常量,如节点模式、等待状态;Node内部有指向其前驱和后继节点的引用(类似双向链表);Node内部有保存当前线程的引用;Node内部的nextWaiter域在共享模式下指向一个常量SHARED,在独占模式下为null或者是一个普通的等待条件队列(只有独占模式下才存在等待条件)。
头尾结点
再看一下AQS中同步等待队列相关的域:
/**
头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
* 同步等待队列的头节点,延迟初始化。除了初始化之外,只能通过setHead方法来改变这个域。
注:如果头结点存在,那么它的waitStatus可以保证一定不是CANCELLED。
*/
private transient volatile Node head;
/**
阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表
* 同步等待队列的尾节点,延迟初始化。只有通过enq方法添加一个新的等待节点的时候才会改变这个域。
*/
private transient volatile Node tail;
注意了,之后分析过程中所说的 queue,也就是阻塞队列不包含 head,不包含 head,不包含 head。
![aqs-0](https://i-blog.csdnimg.cn/blog_migrate/fa8a7b8af6f6a24a84ecf4be94f2b98c.png)
头结点与当前线程
// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
// 当前线程和拥有锁的线程是否为同一线程,即不是的话就排它
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
相关设计模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:
isHeldExclusively()
:该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)
:。尝试获取资源(独占方式),成功则返回true,失败则返回false。tryRelease(int)
:尝试释放资源(独占方式),成功则返回true,失败则返回false。tryAcquireShared(int)
:尝试获取资源(共享方式)。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)
:尝试释放资源(共享方式),如果释放后允许唤醒后续等待结点返回true,否则返回false。
ReentrantLock为例(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。
注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。
以CountDownLatch为例,任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
在acquire()、acquireShared()两种方式下,线程在等待队列中都是忽略中断的,acquireInterruptibly()/acquireSharedInterruptibly()是支持响应中断的。
五、锁的状态state
AQS中有一个这样的属性定义,这个对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
- 当state=0时,表示无锁状态
- 当state>0时,表示已经有线程获得了锁,也就是state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁
// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁
// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1
private volatile int state;
需要注意的是:不同的AQS实现,state所表达的含义是不一样的。
private volatile int state;
// 没有用cas的get和set是在release时用的,因为release的线程肯定有锁,无需用cas
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);
}
上面已经看到AQS内部的整体数据结构,一个同步等待队列+一个(原子的)int域。下面来从请求和释放两条主线来进行相关代码分析。
分析ReentrantLock源码
相比 synchronized,ReentrantLock 增加了一些高级功能。主要来说主要有三点:① 等待可中断;lock.lockInterruptibly() ② 可实现公平锁;③ 可实现选择性通知(锁可以绑定多个条件)
六、同步器sync
sync是一个静态内部类,它继承了AQS这个抽象类(同时Sync还是抽象的),前面说过AQS是一个同步工具,主要用来实现同步控制。我们在利用这个工具的时候,会继承它来实现同步控制功能。(设计模式的模板方法设计模式)
通过进一步分析,发现Sync这个类有两个具体的实现,分别是 NofairSync(非公平锁)
, FailSync(公平锁)
。下面是类之间的关系
public class ReentrantLock implements Lock, java.io.Serializable
//下面3个都是ReentrantLock的内部类
// Sync继承了AQS
abstract static class Sync extends AbstractQueuedSynchronizer
//下面两个继承了Sync,简介继承了AQS
static final class NonfairSync extends Sync
static final class FairSync extends Sync
重新看lock方法
// ReentrantLock.lock()
public void lock(){
sync.lock();// sync为构造ReentrantLock实例时根据传入的bool后new出来NonfairSync实例或FairSync实例
}
调用流程:lock—>acquire->tryAcquire
- nonfairTryAcquire尝试拿锁,如果锁空闲或者完成锁重入,那么就代表拿锁成功。否则返回false拿锁失败
- tryRelease:尝试释放,state状态值-1,减后如果state为0就释放
①NonfairSync非公平锁
非公平同步器:先CAS争抢,争抢不到再排队
实现lock和tryAcquire
- 非公平锁lock时直接cas获取锁一下,获取到就把当前线程设置为持有锁的线程,即把AQS的exclusiveOwnerThread设置为持有锁的线程
- 一次尝试没获取锁成功就去acquire(1)竞争锁
- 流程:NonfairSync.lock()–>AQS.acquire()–>NonfairSync.tryAcquire()–>NonfairSync.nonfairTryAcquire()
// 内部类NonfairSync
static final class NonfairSync extends Sync {
// 非公平锁不排队,直接cas一下
final void lock() {
// 通过cas修改state状态,表示争抢锁的操作。只要cas加锁成功,那么就执行,不去排队,抢了别人的所以叫非公平锁
if (compareAndSetState(0, 1))
// 争抢到了state,设置排他锁。
// 该方法仅仅是把exclusiveOwnerThread设置为当前线程而已,这也head结点就可以不用存储其他内容了,直接拿该属性就能获取当前获取到锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 抢占锁失败,即state不为0。调用acquire来走锁竞争逻辑 // 这个1指的是如果可以重入的话就+1
acquire(1);
}
/*说明:
tryAcquire在AQS为抛异常的方法,所以子类要用必须重写
tryAcquire的意义就是独占模式中尝试获取锁
在独占模式下尝试请求(控制权)。这个方法(实现)应该查看一下对象的状态是否允许在独占模式下请求,如果允许再进行请求。
*
* 这个方法总是被请求线程执行,如果方法执行失败,会将当前线程放到同步等待队列中(如果当前线程还不在同步等待队列中),
直到被其他线程的释放操作唤醒。可以用来实现Lock的tryLock方法。
*/
// 独占模式中尝试获取锁,如果成功就返回true,失败返回false // 它是重写AQS类中的tryAcquire方法,并且大家仔细看一下AQS中tryAcquire方法的定义,并没有实现,而是抛出异常。按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成abstract,让子类来实现呀?大家想想为什么
protected final boolean tryAcquire(int acquires) {
// 非公平锁的尝试获取锁 // 父类Sync内的方法
return nonfairTryAcquire(acquires);
}
}
//-----------顺便对比一下公平锁和非公平锁-----------
在Sync的nonfairTryAcquire()
里还有一次cas尝试获取锁的操作,所以非公平锁有2次直接cas获取锁的机会。2次都获取不到才返回false。
而公平锁上来就判断state是否为0,是0还不够,还得判断一下有没有队列,没有队列才敢cas。、
//Sync内部类方法
final boolean nonfairTryAcquire(int acquires) {//非公平锁NonfairSync的tryAcquire会调用 //公平锁FairSync的tryAcquire没调用,只是返回bool
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();//获取state的值
// --------情况1----------
if (c == 0) {//如果state为0代表无锁状态
if (compareAndSetState(0, acquires)) { //非公平锁第二次cas ,第一次cas在lock()刚开始就进行了
// 用cas拿锁成功,设置为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
// --------情况2----------
// 状态值不为0,但是拿锁的线程是当前线程,那么就重入
else if (current == getExclusiveOwnerThread()) { // 非公平锁和公平锁都支持重入
// 增加重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// --------情况3----------
// 尝试拿锁失败
return false;
}
②FairSync公平锁
实现lock和tryAcquire
- 公平锁直接去排队竞争锁,不能直接cas
- state=0是获取锁的大前提,而队列==null更是公平锁的前提
- 队列==null 才有权利去cas进行改变state
- 持有锁的线程和当前线程一样也有权利设置state
- acquire会先调用NonFair/Fair的tryAcqiure(),然后才执行入队操作。
acquire
其实acquire中有个重要的逻辑是
- 先看看能不能获取锁
- 不能获取锁就去排队
//static final class FairSync extends Sync {
// 公平锁直接去排队竞争锁,不能直接cas
final void lock() {
acquire(1);
}
// 上面并没有重写acquire,但是重写了lock和tryAcquire,所以会调用到重写的
// 公平锁和非公平锁的lock()都是调用acquire(1)//公平锁是直接调用,非公平锁是cas拿不到才调用
public final void acquire(int arg) {
// 公平锁与非公平锁都重载了自己的tryAcquire()
if (!tryAcquire(arg) && //尝试获取独占锁,获取成功tryAcquire返回true,获取失败返回false后继续调用acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 没获取到锁,那么就添加到队列 //addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。// Node.EXCLUSIVE为null
// 将当前线程封装成Node添加到AQS队列尾部
selfInterrupt();
}
- tryAcquire是尝试获取锁
- acquireQueued是入队
- selfInterrupt是阻塞
tryAcquire
下面的非公平锁的获取法
/**
- 第一步判断锁是不是自由状态,如果是则判断直接是否需要排队(
---hasQueuedPredecessors方法判断队列是否被初始化(如果没有初始化显然不需要排队)和是否需要排队(队列如果被初始化了,则自己有可能需要排队));
---如果hasQueuedPredecessors返回false,由于取反了故而不需要排队则进行CAS操作去上锁,如果需要排队则不会进入if分支当中,也不会进else if,会直接返回false表示加锁失败。
- 第二步如果不是自由状态则判断是不是重入,判断持有锁的线程是不是当前线程,如果不是则直接返回false加锁失败,如果是重入则把计数器+1。也说明了reentrantLock是可重入锁。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// ----情况1:等待队列为空,去cas
if (c == 0) { // 锁空闲也不能直接占用,后面可能有排队,先去排队
// 不是直接加锁,而是先入队,这才是公平锁
if (!hasQueuedPredecessors() && // 是否有前任//加!后的含义:判断是否下一个轮到自己了,要轮到自己了就去cas一下,免得还得进行入队操作
compareAndSetState(0, acquires)) {
// 当前线程拿到锁了
//设置当前线程为拥有锁的线程,方面后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入)
setExclusiveOwnerThread(current);
return true;
}
// ---------情况2:锁的重入---------
// 这里不存在并发问题
}else if (current == getExclusiveOwnerThread()) {//当前线程是否跟持有锁的线程是同一线程
//如果C!=0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
//如果C!=0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
//那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
// 思考问题:当前线程释放了锁后怎么办
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// --------情况3:乖乖回去排队-------
return false;
}
队列的前任hasPre
首先说明:这个函数是因为我们原来判断state是0才进来的,也就是说,刚才判断过没有线程运行,那我们应该可以cas。state不为0的话,根本不执行这个函数,而是去判断一下是否是重入线程。
- 主要是用来判断线程需不需要排队,因为队列是FIFO的,所以需要判断队列中有没有相关线程的节点已经在排队了。有则返回true表示线程需要排队,没有则返回false则表示线程无需排队。
- 判断队列有没有很多前驱结点,如果队列中第二个结点为null或者第二个结点的线程是自己,就去cas(0,1)试一下
- 返回true代表有前任:
- 条件1:
h!=t
:头尾没有指向同一个- 结点超过2个,都不为null
- 结点刚刚由2个变为1个,第二个为null(有没有这种情况,待验证,跟h/t这两个更新顺序有关)
- 条件2:
((s = h.next) == null || s.thread != Thread.currentThread())
只要有一个条件为true即可(s = h.next) == null
:没有第二个结点(我觉得这也该去cas一下,没绕明白这里)s.thread != Thread.currentThread())
:第二个结点的线程不是我- 反正不是第二个结点
- 当前线程 : 既然我是null 或者 我不是老二 那我也没啥必要cas获取锁 说明我还排在后面,去排队
- 条件1:
- 返回false代表没有前任:
- 情况1:h==t:也就表示队列只有一个元素 或为空
- 都为null:没有在等待的(根本没有线程执行) 或者 队列都没有初始化(有线程执行,但是之前没发生过冲突,没有队列),去cas吧
- 指向同一个:当前只有一个在运行,去cas碰碰运气,万一他已经执行完了呢
- 情况2:h!=t 为 true,
((s = h.next) == null || s.thread != Thread.currentThread())
都为false- h!=t 代表了队列必为两个或以上元素 并且 前两个不相同
- (s= h.next )== null 为false 表示 第二个元素不为空
- s.thread != Thread.currentThread() 表示 第二个元素已经是当前线程
- 这种情况就是有2个结点,但是第二个结点是我
- 总结:①h/t都为null、②第二个结点的线程是我
- ①没有初始化队列的时候,h、t都为null,那么没有线程在执行,或者只有一个线程在执行,但没有创建队列(当然也就没用ht了)
- ②有队列,但是第二个结点就是我(除去正在运行的,下个就该我了)
- 以上两种情况有权cas一下,免得入队耗时
- 情况1:h==t:也就表示队列只有一个元素 或为空
要想去cas得满足:
//AQS.java
// tryAcquire里 锁空闲 判断自己是否需要排队 //有没有很多前驱
public final boolean hasQueuedPredecessors() {
// 如果没有队列的时候,h和t都是null,返回false,说明他不需要排队,用cas加锁
// 如果队列被初始化的时候,如果队列中元素>1,队列中元素==1,
// >1的时候,第一个h != t满足,头结点的后继结点不为空,当前线程不是头结点的后继结点 ,不满足
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
// h!=t且没有第二个结点了,就返回true
// h!=t且有第二个结点,但是第二个结点的线程和当前线程一样,返回true
}
执行完上面获取锁成功就无需进行队列操作了(当然得遵循公平/非公平的原则),获取锁失败就去入队。
此时的入队是真的要入队,非公平锁也不是cas了,而是先创建Node结点再操作cas的事
// 上面并没有重写acquire,但是重写了lock和tryAcquire,所以会调用到重写的
// 公平锁和非公平锁的lock()都是调用acquire(1)//公平锁是直接调用,非公平锁是cas拿不到才调用
public final void acquire(int arg) {
// 公平锁与非公平锁都重载了自己的tryAcquire()
if (!tryAcquire(arg) && //尝试获取独占锁,获取成功tryAcquire返回true,获取失败返回false后继续调用acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 没获取到锁,那么就添加到队列 //addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。// Node.EXCLUSIVE为null
// 将当前线程封装成Node添加到AQS队列尾部
selfInterrupt();
}
对比公平与非公平
对比两种tryAcquire
调用的是FairSync、NonFairSync子类的tryAcquire()方法,我们可以通过观察下面得知:
- 都是进行了2个判断,①如果state==0,去cas一下②如果≠0,去看看是否是可重入的情况。
- 如果都是上面2个判断,那就没有公平性可言了,所以公平锁的不仅要保证state=0更要求队列为空,要看看队列是否只有一个在运行,或者第二个该自己运行了,才有机会cas,这样就保证公平性了,有其他人在排队就不能cas
- tryAcquire()不成功就走排队的逻辑
再贴一遍没有注释的代码,方便对比
// NonfairSync的nonfairTryAcquire(),间接调用的是Sync
// 非公平
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;
}
//FairSync公平
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;
}
七、入队
回顾下上面发生了什么:
- tryAcquire获取锁,当然很可能没资格或者获取锁失败
- 把线程包装为Node,去入队
- 入队主要是设置tail结点
- 入队后阻塞
// 公平锁和非公平锁的lock()都是调用acquire(1)//公平锁是直接调用,非公平锁是cas拿不到才调用
public final void acquire(int arg) {
// 公平锁与非公平锁都重载了自己的tryAcquire()
if (!tryAcquire(arg) && //尝试获取独占锁,获取成功tryAcquire返回true,获取失败返回false后继续调用acquireQueued
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 没获取到锁,那么就添加到队列 //addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。// Node.EXCLUSIVE为null
// 将当前线程封装成Node添加到AQS队列尾部
selfInterrupt();
}
很明显,入队操作的语句是:acquireQueued
(addWaiter
(Node.EXCLUSIVE), arg)
- addWaiter:没有获取锁成功,把当前线程放到队尾。
- 如果addWaiter第一次尝试没有后没有把当前线程的node放到队尾成功,则调用enq方法去while{cas}
- enq:给addWaiter服务用的,把操作队尾的cas循环和队列初始化放到这里了,addWaiter调用它enq
- acquireQueued
更换新尾结点addWaiter
//addWaiter:获取锁失败,把当前线程封装为Node,添加到AQS队列。将Node作为参数,通过自旋去尝试获取锁。
private Node addWaiter(Node mode) { // 排队过来的mode为null
// 把当前线程封装为Node
Node node = new Node(Thread.currentThread(), mode);//mode为Node.EXCLUSIVE为null
// 获取当前队尾结点 Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 其实下面的操作都是入队的操作,没有其他的操作。入队后返回入队元素node到acquireQueued
// 队尾不为空,说明队列中有结点在排队了,所以就当前线程去末尾排队
if (pred != null) { // head==tail时队列尾空的
// 当前结点的前向指针为上一个尾结点
node.prev = pred;
// cas替换尾结点
if (compareAndSetTail(pred, node)) { // 期望替换的是pred这个尾结点,但如果他被其他线程替换了之后,当前线程就替换不成功了
// 上一个尾结点的后继结点为当前结点
pred.next = node;
return node;
}
}
// 执行到这里有两种情况:1)tail=null 2)tail被其他线程更改
// 对于2)在enq()里重新while{CAS}吧,设置成功后在里面阻塞,被唤醒后才返回
// 对于1) 队尾tail=null。这是因为第一个线程
// 第一个线程进来的时候一定走这,初始化队列,直接把当前结点设置为头结点,表示正在运行的线程
enq(node); // 执行完这里都会把线程入队尾
// 返回当前线程
return node;
}
入队情况讨论enq():
队列有两种状态:假设要入队的结点为A
- 队列不为空:直接while{cas}入队即可
- 队列为空:通过tail=null可以判断队列为空。此时要添加当前要阻塞的结点时,因为原来没有head结点,所以要随便设置个head结点。
- head结点设置为空即可,因为执行完后可以在队列中取到下个要执行的结点
- 此外head结点不需要设置任何信息的原因是前面
exclusiveOwnerThread
属性保存着持有锁的线程 - tail=head,然后回到上面的步骤重新在这个tail后面入队结点A
流程:
- 要放到队尾,必须得先有队头才能入队,没有对头将创建个空队头head(而不是把我们的线程当作对头),同步这个空对头先设置为队尾tail
- 当然设置的过程中得保证同步,用的是while(cas)操作,cas不成功就下一次循环
- 设置成功就return前驱结点
// 前面说了两种情况会来这:队列为空 或 队列有值但是cas替换尾结点时替换失败返回false(被其他线程cas抢了)
private Node enq(final Node node) { //enqueue入队,传进来的node是要放到队尾的当前线程node
for (;;) {
// 获取尾结点(最新的)
Node t = tail;
// 如果尾结点为空,代表队列空,即代表队列没人使用过还没初始化。//同时设置头结点和尾结点,头结点随便设置,尾结点设为当前结点
if (t == null) { // 进入这只用作初始化队列使用,全程只会进入一次,后面tail总是有值,head==tail即代表对列为空
// 设置头结点的过程中如果返回false代表被其他线程创建头结点了
if (compareAndSetHead(new Node())) //为什么随便new了个Node?因为走到这说明已经有个线程在执行了获取了锁,我们创建个队列把当前线程放入队列。而正常来说我们要把头结点当做正在执行的线程,但没有办法了,我们第一个获取到锁的线程并没有给我们留下信息,那我们干脆随便new个头结点让他代表正在执行的线程就行了,我们方面把当前线程添加到他后面就行 //CAS头结点的时候要求原值位null才能cas成功
tail = head;// 把没有信息的头结点为设置为尾结点,而我们当前线程的结点continue下个再设置
// 这里仅仅设置了head,而tail虽然设置了,但不是我们当前线程的node,所以continue下次循环,把当前线程设置为tail
// 一定要搞懂真正的当前线程的tail没在这次设置,否则你会疑惑为什么这里tail不用cas设置?因为这个tail根本就没有用,被其他线程设置了也无所谓
// 还是要搞懂一下为什么这里不需要加锁,我们假设要插入的结点为node
// ①如果tail为空,cas头结点成功,那么tail=head=new Node,但是node还没插入,下次循环把node插入,间情况③④
// ②如果tail为空,cas头结点失败,头结点被其他线程抢了(非公平锁时)。下次循环tail和head都不为空了,进入③④情况
// ③tail不为空,顺利拿到尾结点,然后更新尾结点成功即可
// ④tail不为空,拿到了尾结点后,又被其他线程抢先注册新的尾结点了,进入下一次if重新CAS node结点
// 总结:有for了这里不用把代码块加锁,不成功再cas一次即可。
// 头结点只有刚开始的时候为空,一旦有元素入队后就不会为空了(释放的情况再说)
} else {
// 队列被初始化过了// 到这的原因是原来要添加尾结点的,但是cas时原来尾结点被其他线程更改了,cas没成功,这里重新cas
// 获取原来尾结点,然后当前线程重新替换该尾结点 // 其实就相当于addWaiter里循环cas
// 这个t很可能是我们刚才创建的空结点
node.prev = t;
if (compareAndSetTail(t, node)) {//如果又失败了怎么办?反正外面有for,失败了重新来就行,直到放到队尾
t.next = node;
return t;//返回前驱结点
}
}
}
// 队头里的thread永远为空
}
为什么要返回前驱结点:因为要设置前驱结点为SIGNAL,这样前驱结点就知道要唤醒后继结点了。
八、阻塞挂起acquireQueued
更新完尾结点了,而且返回了倒数第二个结点(阻塞结点的前结点)。下面应该负责线程挂起和唤醒的逻辑
注意一下:如果
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
返回true的话,意味着上面这段代码将进入打断selfInterrupt(),所以正常情况下,下面应该返回false
前面addWaiter和end入队成功后,把刚加入的tail作为参数传给acquireQueued()
-
入参是新队尾的前驱结点,刚才入队成功后,在acquireQueued()中循环2次
-
获取当前线程结点node的前驱结点p(不要用尾tail拿,可能已经被改变了)
-
如果当前运行的结点是前驱结点p,就尝试获取锁。获取锁成功就把当前结点设置为head
-
获取锁失败就看看是要进行下一次循环还是阻塞线程
-
如果前驱结点就是head,代表前驱结点(代表的线程)正在执行,这时刚入队的线程就可以tryAcquire了,因为下个该自己了。
-
如果前驱结点不是head,进行阻塞
// 尝试获取独占锁失败,当前线程添加到尾结点后,循环询问下一个是不是该我了, // 询问队列
final boolean acquireQueued(final Node node, int arg) {//参数node为尾结点(获取刚刚还是尾结点) //arg一般为1
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取尾结点的前驱结点(也可能不是尾结点了,但刚刚还是尾结点) // 下次循环可能还进来,但前驱结点相对头结点的位置是变化的,一旦前驱结点的称为头结点,当前结点就应该去准备获取锁了
final Node p = node.predecessor();
// 如果尾结点为队里中除头结点外唯一结点(只有前驱为head才有资格进行锁的争夺) // 如果获取锁失败又重新插入到尾结点
// 如果下一个该我了再看前一个执行完没有
if (p == head && tryAcquire(arg)) { // 当前线程的上个结点为头结点,且获取到了锁才进入if
// 获取了锁,将当前结点设置为头结点,表示我开始运行了。头结点是获得了锁的结点
setHead(node);
// 原来头结点的后继结点为null,帮助GC
p.next = null; // help GC
// 没有失败,获取锁成功
failed = false;
// 传递中断状态,并返回
return interrupted;//正常情况下死循环唯一的出口
}
// 前驱结点还没有执行/执行完,不用急,还轮不到我,那我可以先挂起让出cpu
// 现在是获取失败了,那应不应该挂起呢 // 根据结点的waitStatus决定是否需要挂起线程
if (shouldParkAfterFailedAcquire(p, node) && // 传入了前驱结点和当前结点
parkAndCheckInterrupt()) // 如果应该没阻塞,那么就阻塞,并检测终端状态
// 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志
// LockSupport.park(this);
interrupted = true;
}
} finally { // 唤醒后才执行
// tryAcquire() 方法抛异常的情况
if (failed)// 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
cancelAcquire(node);
}
}
自旋或直接挂起
shouldParkAfterFailedAcquire:
还在循环cas,检查是否该阻塞本线程,先暂停循环
- 通过前驱结点的waitStatus来判断本线程怎么办
- waitStatus初始为0
- waitStatus==0,则把cas设置为-1(SIGNAL),代表该前驱结点要唤醒后继结点。退出本次shouldParkAfterFailedAcquire(),下次再进来为-1就直接阻塞了
- waitStatus==-1,它知道有后继结点了,告诉后继结点放心睡觉吧,到点了我叫你
- waitStatus>0,代表前驱结点任务取消了,向前找到一个没有取消的结点。
- 什么时候被唤醒,后面说
// 获取锁失败,查看当前线程是否要挂起(阻塞)
private static boolean shouldParkAfterFailedAcquire(Node pred,
Node node) { // 参数为前驱结点和当前结点
// 获取前驱结点的等待状态waitStatus // 前面并没有改过,所以是0
int ws = pred.waitStatus;
// 如果前驱结点为SIGNAL代表当前结点已经声明了需要唤醒,那么就可以阻塞当前结点了,返回true
// 一个节点在其被阻塞之前需要线程"声明"一下其需要唤醒(就是将其前驱节点的等待状态设置为SIGNAL,注意其前驱节点不能是取消状态,如果是,要跳过)
// 如果前驱结点的waitStatus为-1 SIGNAL,说明前驱结点具有唤醒后继结点的功能,直接返回后接着进行挂起操作就可以,以后前驱结点执行的时候会判断到他需要唤醒别的结点,就把当前结点唤醒了
if (ws == Node.SIGNAL)//-1 // 当前线程需要被unpark唤醒 // 为什么不直接=0时候就park?因为为了让他多自旋一次。此外0时候会做一些事情
return true;
// 前驱结点 CANCELLED==1,说明前驱结点的线程被取消了,也就不会唤醒其他结点了,我们如果把他放到我们当前结点的前面,那么当前结点就没人唤醒了,所以我们要把前面被取消的结点过滤掉,连接到个有效的前驱结点,让当前结点能被前面的结点唤醒。
//从前驱节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。 // 直接把他们丢弃了 他们进入不了线程了
if (ws > 0) { // 设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点'的前继节点”。
do {
node.prev = pred = pred.prev;//把取消的结点扔掉
} while (pred.waitStatus > 0);
pred.next = node;
} else { // CONDITION-2/PROPAGATE-3/0 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
/*
前面的源码中,都没有看到有设置waitStatus的,所有每个新的node入队时,waitStatus都为0(成员属性都默认值0)
正常情况下,前驱结点就是之前的tail,那么它的waitStatus应该是0
用CAS将前驱结点的waitStatus设置为NODE.SIGNAL,然后返回false,然后acquireQueue就进入下次循环,再执行到shouldParkAfterFailedAcquire的时候,就进入前面的if返回true
这样当前结点就能被他唤醒了
接下来方法会返回false,还会继续尝试一下请求,以确保在阻塞之前确实无法请求成功。
改的是上一个结点的
上一个结点本来是0,然后改成1
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//前驱结点改为-1
// 最后去执行阻塞的时候,当前线程的ws还是0,上上一个结点是-1了
}
// 方法返回false,再走一次之前函数里的for循环
// 然后再次来到此方法,然后进入第一个if返回true
// 为什么不自己改成-1而是让别人来改呢?他阻塞了不能改为-1了,我自己看不到自己睡觉
return false;
}
/*
接下来说说如果 shouldParkAfterFailedAcquire(p, node) 返回false的情况:仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,
前驱节点的 waitStatus=-1 是依赖于后继节点设置的。
也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。
*/
阻塞线程
// 当前线程应该被挂起阻塞,去执行阻塞
private final boolean parkAndCheckInterrupt() {//如果shouldParkAfterFailedAcquire返回了true,则会执行: parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。
// 真正地负责阻塞当前线程 // 在这阻塞了
LockSupport.park(this);//LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:park()和unpark()
//线程被唤醒,方法返回当前线程的中断状态,并重置当前线程的中断状态(置为false)。
// 为啥要判断线程是否中断过呢,因为如果线程在阻塞期间收到了中断,唤醒(转为运行态)获取锁后(acquireQueued 为 true)需要补一个中断
return Thread.interrupted();
}
LockSupport线程类
LockSupport
类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute, long time);
unpark函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。 permit相当于0/1的开关,默认是0,调用一次unpark就加1变成了1.调用一次park会消费permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成1.这时调用unpark会把permit设置为1.每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会累积
结点状态的转变
- 刚创建结点是mode为Node.EXCLUSIVE为null
- cas
九、解锁unlock
解锁我们把公平和非公平锁写一起即可,因为只有非公平锁才会在上个线程解锁后不排队直接来抢锁。如果他没抢到锁就去排队了,抢到的话本来该执行的那个节点重新连接到头结点。
- ①调用release()的对象不是持有锁的线程:报错
- ②state还不为0,只是减少可重入次数
- ③state变为0了,把exclusiveOwnerThread属性变为null,释放锁的同时唤醒头结点
lock.lock(),调用AQS的release(),其内部调用子类的tryRelease(),ReentrantLock持有公平/非公平的对象
// ReentrantLock.java
public void unlock() {
sync.release(1);
}
用tryRelease释放锁,释放成功后,检查头结点的waitStatus
- 解锁自己,
state--
- 如果state==0,代表没有重复
- 如果state!=0,代表有重入,不能唤醒后继
- 唤醒后继结点unparkSuccessor(传入head)
// AQS.java
public final boolean release(int arg) { // 参数为要减少的重入次数
if (tryRelease(arg)) {//tryRelease防御true代表可重入锁释放完了 // 判断完if后已经把state变为0,可能非公平锁已经又抢了,而该非公平锁线程只是设置了个state和当前占用线程,并没有设置任何node相关
Node h = head;
// 接下来要考虑释放后非公平就抢到了state的情况
if (h != null && // head为空的话没有线程排队,执行释放完成,无需唤醒其他线程。别的线程先不要设置头结点,直接执行即可
h.waitStatus != 0) //防止队列中还有线程,但是该线程还未阻塞。前面说过,线程再阻塞自己前必须设置前驱结点状态为SIGNAL,否则不会唤醒自己
// 把头结点的后继结点unpark。常规流程是进入后unpark那个后继结点的线程,而且unpart后,接下来执行的其实是我们之前获取锁时候阻塞的地方,从阻塞的地方接着去自旋获取锁,回到acQuireQueued()
unparkSuccessor(h);//unpark后继者
return true;
}
return false;//返回false代表还有重入锁没有释放完
}
尝试释放当前线程结点,就是对state进行操作
设置exclusiveOwnerThread为空,其他线程就可以设置了
为什么下面set和get时不用加锁:因为只有持有锁的线程才会执行release,而那个线程只有一个线程,只能可能发生并发
// ReentrantLock
protected final boolean tryRelease(int releases) {
// state-1 // 由于该方法的线程必然持有锁,所以无需加同步操作。(其他向cas从0遍1的操作不会成功,而其他这个cas不成功就拿不到锁,而对于可冲入锁,他都是一个线程,一个线程不可能多个地方同时执行)
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
// 如果释放的线程和获取锁的线程不是同一个,抛出非法监视器状态异常
throw new IllegalMonitorStateException();
boolean free = false;// 标记锁是否空闲
// 如果释放后锁为空了,就清空state后,返回true
if (c == 0) { //在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。
// 由于重入的关系,不是每次释放锁c都等于0,
// 直到最后一次释放锁时,才会把当前线程释放
free = true;
setExclusiveOwnerThread(null);
}
// 更新state值,这里无需加锁
setState(c);//设置为0后非公平锁就会开始抢了,他可能在这就抢到了,而我们公平锁还要往下执行
return free;
}
唤醒后继
释放完了原来线程的锁,唤醒头结点的后继结点
- 如果没有后继结点,或者后继结点被取消了:从tail往前遍历结点,获取从头数第一个没被取消的结点s
- 如果s有值,唤醒该s;如果s为null,代表没有线程阻塞获取该锁,直接结束
// AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {//继任者 // 参数为已经执行完的head // 此时可能已经被非公平锁的其他线程抢了,这里只是拿队首第一个node尝试获得锁 // 传入的是头结点
/*
* 如果node的等待状态为负数(比如:可能需要一个信号),尝试去清空
* "等待唤醒"的状态(将状态置为0),即使设置失败,或者该状态已经被正在等待的线程修改,也没有任何影响。
*/
int ws = node.waitStatus;
// 如果头结点的waitStatus为负的,把头结点waitStatus改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//设置head的状态为0,失败了也无所谓
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
/*
* 需要唤醒的线程在node的后继节点,一般来说就是node的next引用指向的节点。
* 但如果next指向的节点被取消或者为null,那么就同步等待队列的队尾反向查找离
* 当前节点最近的且状态不是"取消"的节点。
*/
Node s = node.next;//头结点的后继结点
// 如果没有后继结点 或 头结点的waitStatus>0即CANCEL(被取消了)
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾结点向前遍历,拿到从前往后的第一个能被唤醒的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
//至于为什么从尾部开始向前遍历,因为在doAcquireInterruptibly.cancelAcquire方法的处理过程中只设置了next的变化,没有设置prev的变化,在最后有这样一行代码:node.next = node,如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有prev是稳定的
s = t;
}
// 拿到了倒霉蛋,但是倒霉蛋被唤醒后可能又陷入阻塞
//内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁
if (s != null)//如果存在(需要唤醒的节点),将该节点的线程唤醒。
LockSupport.unpark(s.thread);//释放后继结点,让他去获取cpu,虽然他可能还得去阻塞
}
上面非公平锁分析完了,但哪里不公平了?
答案是线程刚进来要获取锁的时候,如果直接cas就是非公平了。如果先去排队尾再cas就公平
取消排队
// acquireQueued最后finally块中的cancelAcquire方法。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//跳过首先将要取消的节点的thread域置空。
node.thread = null;
//跳过状态为"取消"的前驱节点。
Node pred = node.prev;
//node前面总是会存在一个非"取消"状态的节点,所以这里不需要null检测。
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext节点(node节点前面的第一个非取消状态节点的后继节点)是需要"断开"的节点。
// 下面的CAS操作会达到"断开"效果,但(CAS操作)也可能会失败,因为可能存在其他"cancel"
// 或者"singal"的竞争
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,那么删除当前节点(将当前节点的前驱节点设置为尾节点)。
if (node == tail && compareAndSetTail(node, pred)) {
//将前驱节点(已经设置为尾节点)的next置空。
compareAndSetNext(pred, predNext, null);
} else {
//如果当前节点不是尾节点,说明后面有其他等待线程,需要做一些唤醒工作。
// 如果当前节点不是头节点,那么尝试将当前节点的前驱节点
// 的等待状态改成SIGNAL,并尝试将前驱节点的next引用指向
// 其后继节点。否则,唤醒后继节点。
int ws;
if (pred != head &&
( (ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)) )
&& pred.thread != null) {
//如果当前节点的前驱节点不是头节点,那么需要给当前节点的后继节点一个"等待唤醒"的标记,
//即 将当前节点的前驱节点等待状态设置为SIGNAL,然后将其设置为当前节点的后继节点的前驱节点....(真绕!)
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//否则,唤醒当前节点的后继节点。
unparkSuccessor(node);
}
//前面提到过,取消节点的next引用会指向自己。
node.next = node; // help GC
}
}
总结
不需要排队的两种情况
AQS的
1、队列没有初始化,则不需要排队,直接去加锁,但是可能会失败;为什么会失败呢? 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;但是肯定有一个会失败,这个时候他就会初始化队列并排队。
2、队列被初始化了,但是当前线程过来加锁,发觉队列当中头结点h就是自己,比如重入,因此不需要排队。
h != t 判断首不等于尾这里要分三种情况
1、队列没有初始化,也就是第一个线程来加锁,h和t都是null,&&运算所以后面不执行,直接返回false,但是这个方法取反了,所以会直接去cas加锁。
第一种情况总结:队列没有初始化,没人排队,那么我也不需要排队,直接上锁,直接去看能不能办理业务。
2、队列被初始化了,后面我们会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;h != t 返回true;但是是&&运算,故而还需要进行后续的判断 ,(有人可能会疑问,比如队列里面只有一个数据,那么头和尾都是同一个怎么会成立呢?其实这是第三种情况–队列里面只有一个数据;这里先不考虑,假设现在队列里面有大于1个数据),继续判断把h.next赋值给s;s有是头的下一个,则表示他是队列当中参与排队的线程而且是排在最前面的;为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;因为h是持有锁的,但是不参与排队;这个也很好理解,比如你去买火车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个h.next肯定不为空;由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况:
2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true------> 最后true)结果: true && true 方法最终放回true,那么去则需要去排队,其实这样符合情理,队列不为空,有人在排队,而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队。
2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程 * 那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() false------> 最后false)结果 true && false 方法最终放回false,那么去则不需要去排队 * 不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;这里又分为两种情况:
<2.2.1>、第一种情况加锁成功?有人会问为什么会成功啊,很简单假如这个时候h也就是持有锁的那个线程执行完了,释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 。
<2.2.2> 、第二种情况加锁失败?有人会问为什么会失败啊。很简单假如这个时候h也就是持有锁的那个线程没执行完,没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if,但是他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放
第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放。
3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?可能有人会说ts加锁的时候里面就只有一个数据;其实不是,因为队列初始化的时候会虚拟一个h作为头结点,当前线程作为第一个排队的节点, 为什么这么做呢?因为aqs认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,而ts其实需要排队的,因为这个时候tf可能没有执行完,ts得不到锁,故而他需要排队;,那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,故而ts无法排在tf之后,只能虚拟一个null节点出来;那么问题来了,究竟什么时候才会出现队列当中只有一个数据呢?假设原先队列里面有5个人在排队,当前面4个都执行完了,轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个 * 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了,所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点 * 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析,记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁 * h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队
第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas。
简单应用
Mutex:不可重入互斥锁,锁资源(state)只有两种状态:0:未被锁定;1:锁定。
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。
十、Condition
我们先来看看 Condition 的使用场景,Condition 经常可以用在生产者-消费者的场景中,请看 Doug Lea 给出的这个例子:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依赖于 lock 来产生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生产
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列已满,等待,直到 not full 才能继续生产
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生产成功,队列已经 not empty 了,发个通知出去
} finally {
lock.unlock();
}
}
// 消费
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列为空,等待,直到队列 not empty,才能继续消费
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消费掉一个,队列 not full 了,发个通知出去
return x;
} finally {
lock.unlock();
}
}
}
1、我们可以看到,在使用 condition 时,必须先持有相应的锁。这个和 Object 类中的方法有相似的语义,需要先持有某个对象的监视器锁才可以执行 wait(), notify() 或 notifyAll() 方法。
2、
ArrayBlockingQueue
采用这种方式实现了生产者-消费者,所以请只把这个例子当做学习例子,实际生产中可以直接使用 ArrayBlockingQueue
我们常用 obj.wait(),obj.notify() 或 obj.notifyAll() 来实现相似的功能,但是,它们是基于对象的监视器锁的。需要深入了解这几个方法的读者,可以参考另一篇文章《深入分析 java 8 编程语言规范:Threads and Locks》。而这里说的 Condition 是基于 ReentrantLock 实现的,而 ReentrantLock 是依赖于 AbstractQueuedSynchronizer 实现的。
ConditionObject实例
每个 ReentrantLock 实例可以通过调用多次 newCondition 产生多个 ConditionObject 的实例:
final ConditionObject newCondition() {
// 实例化一个 ConditionObject
return new ConditionObject();
}
public class ConditionObject implements Condition, java.io.Serializable {
// 条件队列的第一个节点
// 不要管这里的关键字 transient,是不参与序列化的意思
private transient Node firstWaiter;
// 条件队列的最后一个节点
private transient Node lastWaiter;
......
上面我们最要了解的是ConditionObject也有head、tail指针,而且一个lock可以有多个condition队列
条件队列
在上一篇介绍 AQS 的时候,我们有一个阻塞队列,用于保存等待获取锁的线程的队列。这里我们引入另一个概念,叫条件队列(condition queue),我画了一张简单的图用来说明这个。
![condition-2](https://i-blog.csdnimg.cn/blog_migrate/ca084d929a6b8091f46d6ea39e454953.png)
这里,我们简单回顾下 Node 的属性:
volatile int waitStatus; // 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3) volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter;
prev 和 next 用于实现阻塞队列的双向链表,这里的 nextWaiter 用于实现条件队列的单向链表
基本上,把这张图看懂,你也就知道 condition 的处理流程了。所以,我先简单解释下这图,然后再具体地解释代码实现。
- 条件队列和阻塞队列的节点,都是 Node 的实例,因为条件队列的节点是需要转移到阻塞队列中去的;
- 我们知道一个 ReentrantLock 实例可以通过多次调用 newCondition() 来产生多个 Condition 实例,这里对应 condition1 和 condition2。注意,ConditionObject 只有两个属性
firstWaiter
和lastWaiter
; - 每个 condition 有一个关联的条件队列,如线程 1 调用
condition1.await()
方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行,条件队列是一个单向链表; - 调用
condition1.signal()
触发一次唤醒,此时唤醒的是队头,会将condition1 对应的条件队列的 firstWaiter(队头) 移到阻塞队列的队尾,等待获取锁,获取锁后 await 方法才能返回,继续往下执行。
上面的 2->3->4 描述了一个最简单的流程,没有考虑中断、signalAll、还有带有超时参数的 await 方法等,不过把这里弄懂是这节的主要目的。
同时,从图中也可以很直观地看出,哪些操作是线程安全的,哪些操作是线程不安全的。
因为有锁,所以线程安全
await()
// 首先,这个方法是可被中断的,不可被中断的是另一个方法 awaitUninterruptibly()
// 这个方法会阻塞,直到调用 signal 方法(指 signal() 和 signalAll(),下同),或被中断
public final void await() throws InterruptedException {
// 老规矩,既然该方法要响应中断,那么在最开始就判断中断状态
if (Thread.interrupted())
throw new InterruptedException();
// 添加到 condition 的条件队列中
Node node = addConditionWaiter();
// 释放锁,返回值是释放锁之前的 state 值
// await() 之前,当前线程是必须持有锁的,这里肯定要释放掉
int savedState = fullyRelease(node);
int interruptMode = 0;
// 这里退出循环有两种情况,之后再仔细分析
// 1. isOnSyncQueue(node) 返回 true,即当前 node 已经转移到阻塞队列了
// 2. checkInterruptWhileWaiting(node) != 0 会到 break,然后退出循环,代表的是线程中断
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
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);
}
其实,我大体上也把整个 await 过程说得十之八九了,下面我们分步把上面的几个点用源码说清楚。
1. 将节点加入到条件队列
addConditionWaiter() 是将当前节点加入到条件队列,看图我们知道,这种条件队列内的操作是线程安全的。
// 将当前线程对应的节点入队,插入队尾
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果条件队列的最后一个节点取消了,将其清除出去
// 为什么这里把 waitStatus 不等于 Node.CONDITION,就判定为该节点发生了取消排队?
if (t != null && t.waitStatus != Node.CONDITION) {
// 这个方法会遍历整个条件队列,然后会将已取消的所有节点清除出队列
unlinkCancelledWaiters();
t = lastWaiter;
}
// node 在初始化的时候,指定 waitStatus 为 Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t 此时是 lastWaiter,队尾
// 如果队列为空
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
上面的这块代码很简单,就是将当前线程进入到条件队列的队尾。
在addWaiter 方法中,有一个 unlinkCancelledWaiters() 方法,该方法用于清除队列中已经取消等待的节点。
当 await 的时候如果发生了取消操作(这点之后会说),或者是在节点入队的时候,发现最后一个节点是被取消的,会调用一次这个方法。
// 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去
// 纯属链表操作,很好理解,看不懂多看几遍就可以了
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的
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;
}
}
2. 完全释放独占锁
回到 wait 方法,节点入队了以后,会调用 int savedState = fullyRelease(node);
方法释放锁,注意,这里是完全释放独占锁(fully release),因为 ReentrantLock 是可以重入的。
考虑一下这里的 savedState。如果在 condition1.await() 之前,假设线程先执行了 2 次 lock() 操作,那么 state 为 2,我们理解为该线程持有 2 把锁,这里 await() 方法必须将 state 设置为 0,然后再进入挂起状态,这样其他线程才能持有锁。当它被唤醒的时候,它需要重新持有 2 把锁,才能继续下去。
// 首先,我们要先观察到返回值 savedState 代表 release 之前的 state 值
// 对于最简单的操作:先 lock.lock(),然后 condition1.await()。
// 那么 state 经过这个方法由 1 变为 0,锁释放,此方法返回 1
// 相应的,如果 lock 重入了 n 次,savedState == n
// 如果这个方法失败,会将节点设置为"取消"状态,并抛出异常 IllegalMonitorStateException
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
考虑一下,如果一个线程在不持有 lock 的基础上,就去调用 condition1.await() 方法,它能进入条件队列,但是在上面的这个方法中,由于它不持有锁,release(savedState) 这个方法肯定要返回 false,进入到异常分支,然后进入 finally 块设置
node.waitStatus = Node.CANCELLED
,这个已经入队的节点之后会被后继的节点”请出去“。
3. 等待进入阻塞队列
释放掉锁以后,接下来是这段,这边会自旋,如果发现自己还没到阻塞队列,那么挂起,等待被转移到阻塞队列。
int interruptMode = 0;
// 如果不在阻塞队列中,注意了,是阻塞队列
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
// 这里可以先不用看了,等看到它什么时候被 unpark 再说
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue(Node node) 用于判断节点是否已经转移到阻塞队列了:
// 在节点入条件队列的时候,初始化时设置了 waitStatus = Node.CONDITION
// 前面我提到,signal 的时候需要将节点从条件队列移到阻塞队列,
// 这个方法就是判断 node 是否已经移动到阻塞队列了
final boolean isOnSyncQueue(Node node) {
// 移动过去的时候,node 的 waitStatus 会置为 0,这个之后在说 signal 方法的时候会说到
// 如果 waitStatus 还是 Node.CONDITION,也就是 -2,那肯定就是还在条件队列中
// 如果 node 的前驱 prev 指向还是 null,说明肯定没有在 阻塞队列(prev是阻塞队列链表中使用的)
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果 node 已经有后继节点 next 的时候,那肯定是在阻塞队列了
if (node.next != null)
return true;
// 下面这个方法从阻塞队列的队尾开始从后往前遍历找,如果找到相等的,说明在阻塞队列,否则就是不在阻塞队列
// 可以通过判断 node.prev() != null 来推断出 node 在阻塞队列吗?答案是:不能。
// 这个可以看上篇 AQS 的入队方法,首先设置的是 node.prev 指向 tail,
// 然后是 CAS 操作将自己设置为新的 tail,可是这次的 CAS 是可能失败的。
return findNodeFromTail(node);
}
// 从阻塞队列的队尾往前遍历,如果找到,返回 true
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
回到前面的循环,isOnSyncQueue(node) 返回 false 的话,那么进到 LockSupport.park(this);
这里线程挂起。
4. signal 唤醒线程,转移到阻塞队列
为了大家理解,这里我们先看唤醒操作,因为刚刚到 LockSupport.park(this);
把线程挂起了,等待唤醒。
唤醒操作通常由另一个线程来操作,就像生产者-消费者模式中,如果线程因为等待消费而挂起,那么当生产者生产了一个东西后,会调用 signal 唤醒正在等待的线程来消费。
// 唤醒等待了最久的线程
// 其实就是,将这个线程对应的 node 从条件队列转移到阻塞队列
public final void signal() {
// 调用 signal 方法的线程必须持有当前的独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 从条件队列队头往后遍历,找出第一个需要转移的 node
// 因为前面我们说过,有些线程会取消排队,但是可能还在队列中
private void doSignal(Node first) {
do {
// 将 firstWaiter 指向 first 节点后面的第一个,因为 first 节点马上要离开了
// 如果将 first 移除后,后面没有节点在等待了,那么需要将 lastWaiter 置为 null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 因为 first 马上要被移到阻塞队列了,和条件队列的链接关系在这里断掉
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
// 这里 while 循环,如果 first 转移不成功,那么选择 first 后面的第一个节点进行转移,依此类推
}
// 将节点从条件队列转移到阻塞队列
// true 代表成功转移
// false 代表在 signal 之前,节点已经取消了
final boolean transferForSignal(Node node) {
// CAS 如果失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,
// 既然已经取消,也就不需要转移了,方法返回,转移后面一个节点
// 否则,将 waitStatus 置为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// enq(node): 自旋进入阻塞队列的队尾
// 注意,这里的返回值 p 是 node 在阻塞队列的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。唤醒之后会怎么样,后面再解释
// 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用,上篇介绍的时候说过,节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程,之后的操作看下一节
LockSupport.unpark(node.thread);
return true;
}
正常情况下,ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)
这句中,ws <= 0,而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL)
会返回 true,所以一般也不会进去 if 语句块中唤醒 node 对应的线程。然后这个方法返回 true,也就意味着 signal 方法结束了,节点进入了阻塞队列。
假设发生了阻塞队列中的前驱节点取消等待,或者 CAS 失败,只要唤醒线程,让其进到下一步即可。
5. 唤醒后检查中断状态
上一步 signal 之后,我们的线程由条件队列转移到了阻塞队列,之后就准备获取锁了。只要重新获取到锁了以后,继续往下执行。
等线程从挂起中恢复过来,继续往下看
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
先解释下 interruptMode。interruptMode 可以取值为 REINTERRUPT(1),THROW_IE(-1),0
- REINTERRUPT: 代表 await 返回的时候,需要重新设置中断状态
- THROW_IE: 代表 await 返回的时候,需要抛出 InterruptedException 异常
- 0 :说明在 await 期间,没有发生中断
有以下三种情况会让 LockSupport.park(this); 这句返回继续往下执行:
- 常规路径。signal -> 转移节点到阻塞队列 -> 获取了锁(unpark)
- 线程中断。在 park 的时候,另外一个线程对这个线程进行了中断
- signal 的时候我们说过,转移以后的前驱节点取消了,或者对前驱节点的CAS操作失败了
- 假唤醒。这个也是存在的,和 Object.wait() 类似,都有这个问题
线程唤醒后第一步是调用 checkInterruptWhileWaiting(node) 这个方法,此方法用于判断是否在线程挂起期间发生了中断,如果发生了中断,是 signal 调用之前中断的,还是 signal 之后发生的中断。
// 1. 如果在 signal 之前已经中断,返回 THROW_IE
// 2. 如果是 signal 之后中断,返回 REINTERRUPT
// 3. 没有发生中断,返回 0
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
Thread.interrupted():如果当前线程已经处于中断状态,那么该方法返回 true,同时将中断状态重置为 false,所以,才有后续的
重新中断(REINTERRUPT)
的使用。
看看怎么判断是 signal 之前还是之后发生的中断:
// 只有线程处于中断状态,才会调用此方法
// 如果需要的话,将这个已经取消等待的节点转移到阻塞队列
// 返回 true:如果此线程在 signal 之前被取消,
final boolean transferAfterCancelledWait(Node node) {
// 用 CAS 将节点状态设置为 0
// 如果这步 CAS 成功,说明是 signal 方法之前发生的中断,因为如果 signal 先发生的话,signal 中会将 waitStatus 设置为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 将节点放入阻塞队列
// 这里我们看到,即使中断了,依然会转移到阻塞队列
enq(node);
return true;
}
// 到这里是因为 CAS 失败,肯定是因为 signal 方法已经将 waitStatus 设置为了 0
// signal 方法会将节点转移到阻塞队列,但是可能还没完成,这边自旋等待其完成
// 当然,这种事情还是比较少的吧:signal 调用之后,没完成转移之前,发生了中断
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
这里再说一遍,即使发生了中断,节点依然会转移到阻塞队列。
到这里,大家应该都知道这个 while 循环怎么退出了吧。要么中断,要么转移成功。
这里描绘了一个场景,本来有个线程,它是排在条件队列的后面的,但是因为它被中断了,那么它会被唤醒,然后它发现自己不是被 signal 的那个,但是它会自己主动去进入到阻塞队列。
6. 获取独占锁
while 循环出来以后,下面是这段代码:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
由于 while 出来后,我们确定节点已经进入了阻塞队列,准备获取锁。
这里的 acquireQueued(node, savedState) 的第一个参数 node 之前已经经过 enq(node) 进入了队列,参数 savedState 是之前释放锁前的 state,这个方法返回的时候,代表当前线程获取了锁,而且 state == savedState了。
注意,前面我们说过,不管有没有发生中断,都会进入到阻塞队列,而 acquireQueued(node, savedState) 的返回值就是代表线程是否被中断。如果返回 true,说明被中断了,而且 interruptMode != THROW_IE,说明在 signal 之前就发生中断了,这里将 interruptMode 设置为 REINTERRUPT,用于待会重新中断。
继续往下:
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
本着一丝不苟的精神,这边说说 node.nextWaiter != null
怎么满足。我前面也说了 signal 的时候会将节点转移到阻塞队列,有一步是 node.nextWaiter = null,将断开节点和条件队列的联系。
可是,在判断发生中断的情况下,是 signal 之前还是之后发生的?
这部分的时候,我也介绍了,如果 signal 之前就中断了,也需要将节点进行转移到阻塞队列,这部分转移的时候,是没有设置 node.nextWaiter = null 的。
之前我们说过,如果有节点取消,也会调用 unlinkCancelledWaiters 这个方法,就是这里了。
7. 处理中断状态
到这里,我们终于可以好好说下这个 interruptMode 干嘛用了。
- 0:什么都不做,没有被中断过;
- THROW_IE:await 方法抛出 InterruptedException 异常,因为它代表在 await() 期间发生了中断;
- REINTERRUPT:重新中断当前线程,因为它代表 await() 期间没有被中断,而是 signal() 以后发生的中断
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
这个中断状态这部分内容,大家应该都理解了吧,不理解的话,多看几遍就是了。
* 带超时机制的 await
经过前面的 7 步,整个 ConditionObject 类基本上都分析完了,接下来简单分析下带超时机制的 await 方法。
public final long awaitNanos(long nanosTimeout)
throws InterruptedException
public final boolean awaitUntil(Date deadline)
throws InterruptedException
public final boolean await(long time, TimeUnit unit)
throws InterruptedException
这三个方法都差不多,我们就挑一个出来看看吧:
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// 等待这么多纳秒
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// 当前时间 + 等待时长 = 过期时间
final long deadline = System.nanoTime() + nanosTimeout;
// 用于返回 await 是否超时
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 时间到啦
if (nanosTimeout <= 0L) {
// 这里因为要 break 取消等待了。取消等待的话一定要调用 transferAfterCancelledWait(node) 这个方法
// 如果这个方法返回 true,在这个方法内,将节点转移到阻塞队列成功
// 返回 false 的话,说明 signal 已经发生,signal 方法将节点转移了。也就是说没有超时嘛
timedout = transferAfterCancelledWait(node);
break;
}
// spinForTimeoutThreshold 的值是 1000 纳秒,也就是 1 毫秒
// 也就是说,如果不到 1 毫秒了,那就不要选择 parkNanos 了,自旋的性能反而更好
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 得到剩余时间
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
超时的思路还是很简单的,不带超时参数的 await 是 park,然后等待别人唤醒。而现在就是调用 parkNanos 方法来休眠指定的时间,醒来后判断是否 signal 调用了,调用了就是没有超时,否则就是超时了。超时的话,自己来进行转移到阻塞队列,然后抢锁。
* 不抛出 InterruptedException 的 await
关于 Condition 最后一小节了。
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
很简单,贴一下代码大家就都懂了,我就不废话了。
AbstractQueuedSynchronizer 独占锁的取消排队
这篇文章说的是 AbstractQueuedSynchronizer,只不过好像 Condition 说太多了,赶紧把思路拉回来。
接下来,我想说说怎么取消对锁的竞争?
上篇文章提到过,最重要的方法是这个,我们要在这里面找答案:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先,到这个方法的时候,节点一定是入队成功的。
我把 parkAndCheckInterrupt() 代码贴过来:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这两段代码联系起来看,是不是就清楚了。
如果我们要取消一个线程的排队,我们需要在另外一个线程中对其进行中断。比如某线程调用 lock() 老久不返回,我想中断它。一旦对其进行中断,此线程会从 LockSupport.park(this);
中唤醒,然后 Thread.interrupted();
返回 true。
我们发现一个问题,即使是中断唤醒了这个线程,也就只是设置了 interrupted = true
然后继续下一次循环。而且,由于 Thread.interrupted();
会清除中断状态,第二次进 parkAndCheckInterrupt 的时候,返回会是 false。
所以,我们要看到,在这个方法中,interrupted 只是用来记录是否发生了中断,然后用于方法返回值,其他没有做任何相关事情。
所以,我们看外层方法怎么处理 acquireQueued 返回 false 的情况。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
所以说,lock() 方法处理中断的方法就是,你中断归中断,我抢锁还是照样抢锁,几乎没关系,只是我抢到锁了以后,设置线程的中断状态而已,也不抛出任何异常出来。调用者获取锁后,可以去检查是否发生过中断,也可以不理会。
来条分割线。有没有被骗的感觉,我说了一大堆,可是和取消没有任何关系啊。
我们来看 ReentrantLock 的另一个 lock 方法:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
方法上多了个 throws InterruptedException
,经过前面那么多知识的铺垫,这里我就不再啰里啰嗦了。
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())
// 就是这里了,一旦异常,马上结束这个方法,抛出异常。
// 这里不再只是标记这个方法的返回值代表中断状态
// 而是直接抛出异常,而且外层也不捕获,一直往外抛到 lockInterruptibly
throw new InterruptedException();
}
} finally {
// 如果通过 InterruptedException 异常出去,那么 failed 就是 true 了
if (failed)
cancelAcquire(node);
}
}
既然到这里了,顺便说说 cancelAcquire 这个方法吧:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
// 找一个合适的前驱。其实就是将它前面的队列中已经取消的节点都”请出去“
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
其实这个方法没什么好说的,一行行看下去就是了,节点取消,只要把 waitStatus 设置为 Node.CANCELLED,会有非常多的情况被从阻塞队列中请出去,主动或被动。
再说 java 线程中断和 InterruptedException 异常
在之前的文章中,我们接触了大量的中断,这边算是个总结吧。如果你完全熟悉中断了,没有必要再看这节,本节为新手而写。
线程中断
首先,我们要明白,中断不是类似 linux 里面的命令 kill -9 pid,不是说我们中断某个线程,这个线程就停止运行了。中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false。
Java 中的中断和操作系统的中断还不一样,这里就按照状态来理解吧,不要和操作系统的中断联系在一起
关于中断状态,我们需要重点关注 Thread 类中的以下几个方法:
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}
// Thread 中的静态方法,检测调用这个方法的线程是否已经中断
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
// 所以,如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}
// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}
我们说中断一个线程,其实就是设置了线程的 interrupted status 为 true,至于说被中断的线程怎么处理这个状态,那是那个线程自己的事。如以下代码:
while (!Thread.interrupted()) {
doWork();
System.out.println("我做完一件事了,准备做下一件,如果没有其他线程中断我的话");
}
这种代码就是会响应中断的,它会在干活的时候先判断下中断状态,不过,除了 JDK 源码外,其他用中断的场景还是比较少的,毕竟 JDK 源码非常讲究。
当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。
如果线程处于以下三种情况,那么当线程被中断的时候,能自动感知到:
-
来自 Object 类的 wait()、wait(long)、wait(long, int),
来自 Thread 类的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)
这几个方法的相同之处是,方法上都有: throws InterruptedException
如果线程阻塞在这些方法上(我们知道,这些方法会让当前线程阻塞),这个时候如果其他线程对这个线程进行了中断,那么这个线程会从这些方法中立即返回,抛出 InterruptedException 异常,同时重置中断状态为 false。
-
实现了 InterruptibleChannel 接口的类中的一些 I/O 阻塞操作,如 DatagramChannel 中的 connect 方法和 receive 方法等
如果线程阻塞在这里,中断线程会导致这些方法抛出 ClosedByInterruptException 并重置中断状态。
-
Selector 中的 select 方法,参考下我写的 NIO 的文章
一旦中断,方法立即返回
对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),并且在做出相应的操作后都会重置中断状态为 false。
那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。
InterruptedException 概述
它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object 中的 wait() 方法,ReentrantLock 中的 lockInterruptibly() 方法,Thread 中的 sleep() 方法等等,这些方法都带有 throws InterruptedException
,我们通常称这些方法为阻塞方法(blocking method)。
阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait 方法依赖于其他线程的 notify,lock 方法依赖于其他线程的 unlock等等。
当我们看到方法上带有 throws InterruptedException
时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。
除了几个特殊类(如 Object,Thread等)外,感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候,就是通过在合适的时机(通常在循环的开始处)去判断线程的中断状态,然后做相应的操作(通常是方法直接返回或者抛出异常)。当然,我们也要看到,如果我们一次循环花的时间比较长的话,那么就需要比较长的时间才能感知到线程中断了。
处理中断
一旦中断发生,我们接收到了这个信息,然后怎么去处理中断呢?本小节将简单分析这个问题。
我们经常会这么写代码:
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// ignore
}
// go on
当 sleep 结束继续往下执行的时候,我们往往都不知道这块代码是真的 sleep 了 10 秒,还是只休眠了 1 秒就被中断了。这个代码的问题在于,我们将这个异常信息吞掉了。(对于 sleep 方法,我相信大部分情况下,我们都不在意是否是中断了,这里是举例)
AQS 的做法很值得我们借鉴,我们知道 ReentrantLock 有两种 lock 方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
前面我们提到过,lock() 方法不响应中断。如果 thread1 调用了 lock() 方法,过了很久还没抢到锁,这个时候 thread2 对其进行了中断,thread1 是不响应这个请求的,它会继续抢锁,当然它不会把“被中断”这个信息扔掉。我们可以看以下代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 我们看到,这里也没做任何特殊处理,就是记录下来中断状态。
// 这样,如果外层方法需要去检测的时候,至少我们没有把这个信息丢了
selfInterrupt();// Thread.currentThread().interrupt();
}
而对于 lockInterruptibly() 方法,因为其方法上面有 throws InterruptedException
,这个信号告诉我们,如果我们要取消线程抢锁,直接中断这个线程即可,它会立即返回,抛出 InterruptedException 异常。
在并发包中,有非常多的这种处理中断的例子,提供两个方法,分别为响应中断和不响应中断,对于不响应中断的方法,记录中断而不是丢失这个信息。如 Condition 中的两个方法就是这样的:
void await() throws InterruptedException;
void awaitUninterruptibly();
通常,如果方法会抛出 InterruptedException 异常,往往方法体的第一句就是:
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); ...... }
熟练使用中断,对于我们写出优雅的代码是有帮助的,也有助于我们分析别人的源码。
总结
为什么非公平锁性能好
非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前可以进行两次尝试,这大大增加了获取锁的机会。这种好处体现在两个方面:
- 1.线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提升会更加明显。
- 2.减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。除此之外,加锁流程中至少有两处通过将某些特殊情况提前来减少CAS操作的竞争,增加并发情况下的性能。一处就是获取锁时将非重入的情况提前
CountDownLatch
CountDownLatch 这个类是比较典型的 AQS 的共享模式的使用,这是一个高频使用的类。latch 的中文意思是门栓、栅栏,具体怎么解释我就不废话了,大家随意,看两个例子就知道在哪里用、怎么用了。
使用例子
我们看下 Doug Lea 在 java doc 中给出的例子,这个例子非常实用,我经常会写到这个代码。
假设我们有 N ( N > 0 ) 个任务,那么我们会用 N 来初始化一个 CountDownLatch,然后将这个 latch 的引用传递到各个线程中,在每个线程完成了任务后,调用 latch.countDown() 代表完成了一个任务。
调用 latch.await() 的方法的线程会阻塞,直到所有的任务完成。
class Driver2 { // ...
void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = Executors.newFixedThreadPool(8);
// 创建 N 个任务,提交给线程池来执行
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
// 等待所有的任务完成,这个方法才会返回
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
// 这个线程的任务完成了,调用 countDown 方法
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() { ...}
}
所以说 CountDownLatch 非常实用,我们常常会将一个比较大的任务进行拆分,然后开启多个线程来执行,等所有线程都执行完了以后,再往下执行其他操作。这里例子中,只有 main 线程调用了 await 方法。
我们再来看另一个例子,这个例子很典型,用了两个 CountDownLatch:
class Driver { // ...
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
// 这边插入一些代码,确保上面的每个线程先启动起来,才执行下面的代码。
doSomethingElse(); // don't let run yet
// 因为这里 N == 1,所以,只要调用一次,那么所有的 await 方法都可以通过
startSignal.countDown(); // let all threads proceed
doSomethingElse();
// 等待所有任务结束
doneSignal.await(); // wait for all to finish
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
// 为了让所有线程同时开始任务,我们让所有线程先阻塞在这里
// 等大家都准备好了,再打开这个门栓
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() { ...}
}
这个例子中,doneSignal 同第一个例子的使用,我们说说这里的 startSignal。N 个新开启的线程都调用了startSignal.await() 进行阻塞等待,它们阻塞在栅栏上,只有当条件满足的时候(startSignal.countDown()),它们才能同时通过这个栅栏,目的是让所有的线程站在一个起跑线上。
![5](https://i-blog.csdnimg.cn/blog_migrate/aaaceb95f339366c1b107703bedf7cf7.png)
如果始终只有一个线程调用 await 方法等待任务完成,那么 CountDownLatch 就会简单很多,所以之后的源码分析读者一定要在脑海中构建出这么一个场景:有 m 个线程是做任务的,有 n 个线程在某个栅栏上等待这 m 个线程做完任务,直到所有 m 个任务完成后,n 个线程同时通过栅栏。
源码分析
Talk is cheap, show me the code.
构造方法,需要传入一个不小于 0 的整数:
public CountDownLatch(int count) {
// 只能减到0
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 老套路了,内部封装一个 Sync 类继承自 AQS
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
// 这样就 state == count 了
setState(count);
}
...
}
代码都是套路,先分析套路:AQS 里面的 state 是一个整数值,这边用一个 int count 参数其实初始化就是设置了这个值,所有调用了 await 方法的等待线程会挂起,然后有其他一些线程会做 state = state - 1 操作,当 state 减到 0 的同时,那个将 state 减为 0 的线程会负责唤醒 所有调用了 await 方法的线程。都是套路啊,只是 Doug Lea 的套路很深,代码很巧妙,不然我们也没有要分析源码的必要。
对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。
countDown() 方法每次调用都会将 state 减 1,直到 state 的值为 0;而 await 是一个阻塞方法,当 state 减为 0 的时候,await 方法才会返回。await 可以被多个线程调用,读者这个时候脑子里要有个图:所有调用了 await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。
我们用以下程序来分析源码,t1 和 t2 负责调用 countDown() 方法,t3 和 t4 调用 await 方法阻塞:
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException ignore) {
}
// 休息 5 秒后(模拟线程工作了 5 秒),调用 countDown()
latch.countDown();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException ignore) {
}
// 休息 10 秒后(模拟线程工作了 10 秒),调用 countDown()
latch.countDown();
}
}, "t2");
t1.start();
t2.start();
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 阻塞,等待 state 减为 0
latch.await();
System.out.println("线程 t3 从 await 中返回了");
} catch (InterruptedException e) {
System.out.println("线程 t3 await 被中断");
Thread.currentThread().interrupt();
}
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 阻塞,等待 state 减为 0
latch.await();
System.out.println("线程 t4 从 await 中返回了");
} catch (InterruptedException e) {
System.out.println("线程 t4 await 被中断");
Thread.currentThread().interrupt();
}
}
}, "t4");
t3.start();
t4.start();
}
}
上述程序,大概在过了 10 秒左右的时候,会输出:
线程 t3 从 await 中返回了
线程 t4 从 await 中返回了
这两条输出,顺序不是绝对的
后面的分析,我们假设 t3 先进入阻塞队列
接下来,我们按照流程一步一步走:先 await 等待,然后被唤醒,await 方法返回。
await
首先,我们来看 await() 方法,它代表线程阻塞,等待 state 的值减为 0。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 这也是老套路了,我在第二篇的中断那一节说过了
if (Thread.interrupted())
throw new InterruptedException();
// t3 和 t4 调用 await 的时候,state 都大于 0(state 此时为 2)。
// 也就是说,这个 if 返回 true,然后往里看
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 只有当 state == 0 的时候,这个方法才会返回 1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
从方法名我们就可以看出,这个方法是获取共享锁,并且此方法是可中断的(中断的时候抛出 InterruptedException 退出这个方法)。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 1. 入队
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 同上,只要 state 不等于 0,那么这个方法返回 -1
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 2
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们来仔细分析这个方法,线程 t3 经过第 1 步 addWaiter 入队以后,我们应该可以得到这个:
![2](https://i-blog.csdnimg.cn/blog_migrate/d2cf3db8f8e3f2a1722b19c588f5ea41.png)
由于 tryAcquireShared 这个方法会返回 -1,所以 if (r >= 0) 这个分支不会进去。到 shouldParkAfterFailedAcquire 的时候,t3 将 head 的 waitStatus 值设置为 -1,如下:
![3](https://i-blog.csdnimg.cn/blog_migrate/91df5437fb938b75794dc811307471a8.png)
然后进入到 parkAndCheckInterrupt 的时候,t3 挂起。
我们再分析 t4 入队,t4 会将前驱节点 t3 所在节点的 waitStatus 设置为 -1,t4 入队后,应该是这样的:
![4](https://i-blog.csdnimg.cn/blog_migrate/15e6f93a15a5612ea7e97fa2e583447c.png)
然后,t4 也挂起。接下来,t3 和 t4 就等待唤醒了。
接下来,我们来看唤醒的流程。为了让下面的示意图更丰富些,我们假设用 10 初始化 CountDownLatch。
![1](https://i-blog.csdnimg.cn/blog_migrate/b2afeeb5f636fb07bbc55456d96fc928.png)
当然,我们的例子中,其实没有 10 个线程,只有 2 个线程 t1 和 t2,只是为了让图好看些罢了。
我们再一步步看具体的流程。首先,我们看 countDown() 方法:
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 只有当 state 减为 0 的时候,tryReleaseShared 才返回 true
// 否则只是简单的 state = state - 1 那么 countDown() 方法就结束了
// 将 state 减到 0 的那个操作才是最复杂的,继续往下吧
if (tryReleaseShared(arg)) {
// 唤醒 await 的线程
doReleaseShared();
return true;
}
return false;
}
// 这个方法很简单,用自旋的方法实现 state 减 1
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
countDown 方法就是每次调用都将 state 值减 1,如果 state 减到 0 了,那么就调用下面的方法进行唤醒阻塞队列中的线程:
// 调用这个方法的时候,state == 0
// 这个方法先不要看所有的代码,按照思路往下到我写注释的地方,我们先跑通一个流程,其他的之后还会仔细分析
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// t3 入队的时候,已经将头节点的 waitStatus 设置为 Node.SIGNAL(-1) 了
if (ws == Node.SIGNAL) {
// 将 head 的 waitStatue 设置为 0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 就是这里,唤醒 head 的后继节点,也就是阻塞队列中的第一个节点
// 在这里,也就是唤醒 t3
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
一旦 t3 被唤醒后,我们继续回到 await 的这段代码,parkAndCheckInterrupt 返回,我们先不考虑中断的情况:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 2. 这里是下一步
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 1. 唤醒后这个方法返回
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
接下来,t3 会进到 setHeadAndPropagate(node, r) 这个方法,先把 head 给占了,然后唤醒队列中其他的线程:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 下面说的是,唤醒当前 node 之后的节点,即 t3 已经醒了,马上唤醒 t4
// 类似的,如果 t4 后面还有 t5,那么 t4 醒了以后,马上将 t5 给唤醒了
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 又是这个方法,只是现在的 head 已经不是原来的空节点了,是 t3 的节点了
doReleaseShared();
}
}
又回到这个方法了,那么接下来,我们好好分析 doReleaseShared 这个方法,我们根据流程,头节点 head 此时是 t3 节点了:
// 调用这个方法的时候,state == 0
private void doReleaseShared() {
for (;;) {
Node h = head;
// 1. h == null: 说明阻塞队列为空
// 2. h == tail: 说明头结点可能是刚刚初始化的头节点,
// 或者是普通线程节点,但是此节点既然是头节点了,那么代表已经被唤醒了,阻塞队列没有其他节点了
// 所以这两种情况不需要进行唤醒后继节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// t4 将头节点(此时是 t3)的 waitStatus 设置为 Node.SIGNAL(-1) 了
if (ws == Node.SIGNAL) {
// 这里 CAS 失败的场景请看下面的解读
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 就是这里,唤醒 head 的后继节点,也就是阻塞队列中的第一个节点
// 在这里,也就是唤醒 t4
unparkSuccessor(h);
}
else if (ws == 0 &&
// 这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将这个 ws 设置为 -1
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环
// 否则,就是 head 没变,那么退出循环,
// 退出循环是不是意味着阻塞队列中的其他节点就不唤醒了?当然不是,唤醒的线程之后还是会调用这个方法的
if (h == head) // loop if head changed
break;
}
}
我们分析下最后一个 if 语句,然后才能解释第一个 CAS 为什么可能会失败:
- h == head:说明头节点还没有被刚刚用 unparkSuccessor 唤醒的线程(这里可以理解为 t4)占有,此时 break 退出循环。
- h != head:头节点被刚刚唤醒的线程(这里可以理解为 t4)占有,那么这里重新进入下一轮循环,唤醒下一个节点(这里是 t4 )。我们知道,等到 t4 被唤醒后,其实是会主动唤醒 t5、t6、t7…,那为什么这里要进行下一个循环来唤醒 t5 呢?我觉得是出于吞吐量的考虑。
满足上面的 2 的场景,那么我们就能知道为什么上面的 CAS 操作 compareAndSetWaitStatus(h, Node.SIGNAL, 0) 会失败了?
因为当前进行 for 循环的线程到这里的时候,可能刚刚唤醒的线程 t4 也刚刚好到这里了,那么就有可能 CAS 失败了。
for 循环第一轮的时候会唤醒 t4,t4 醒后会将自己设置为头节点,如果在 t4 设置头节点后,for 循环才跑到 if (h == head),那么此时会返回 false,for 循环会进入下一轮。t4 唤醒后也会进入到这个方法里面,那么 for 循环第二轮和 t4 就有可能在这个 CAS 相遇,那么就只会有一个成功了。
CyclicBarrier
字面意思是“可重复使用的栅栏”或“周期性的栅栏”,总之不是用了一次就没用了的,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。看如下示意图,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一个栅栏,因为它的栅栏(Barrier)可以重复使用(Cyclic)。
首先,CyclicBarrier 的源码实现和 CountDownLatch 大相径庭,CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现。
因为 CyclicBarrier 的源码相对来说简单许多,读者只要熟悉了前面关于 Condition 的分析,那么这里的源码是毫无压力的,就是几个特殊概念罢了。
先用一张图来描绘下 CyclicBarrier 里面的一些概念,和它的基本使用流程:
看图我们也知道了,CyclicBarrier 的源码最重要的就是 await() 方法了。
大家先把图看完,然后我们开始源码分析:
public class CyclicBarrier {
// 我们说了,CyclicBarrier 是可以重复使用的,我们把每次从开始使用到穿过栅栏当做"一代",或者"一个周期"
private static class Generation {
boolean broken = false;
}
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
// CyclicBarrier 是基于 Condition 的
// Condition 是“条件”的意思,CyclicBarrier 的等待线程通过 barrier 的“条件”是大家都到了栅栏上
private final Condition trip = lock.newCondition();
// 参与的线程数
private final int parties;
// 如果设置了这个,代表越过栅栏之前,要执行相应的操作
private final Runnable barrierCommand;
// 当前所处的“代”
private Generation generation = new Generation();
// 还没有到栅栏的线程数,这个值初始为 parties,然后递减
// 还没有到栅栏的线程数 = parties - 已经到栅栏的数量
private int count;
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
首先,先看怎么开启新的一代:
// 开启新的一代,当最后一个线程到达栅栏上的时候,调用这个方法来唤醒其他线程,同时初始化“下一代”
private void nextGeneration() {
// 首先,需要唤醒所有的在栅栏上等待的线程
trip.signalAll();
// 更新 count 的值
count = parties;
// 重新生成“新一代”
generation = new Generation();
}
开启新的一代,类似于重新实例化一个 CyclicBarrier 实例
看看怎么打破一个栅栏:
private void breakBarrier() {
// 设置状态 broken 为 true
generation.broken = true;
// 重置 count 为初始值 parties
count = parties;
// 唤醒所有已经在等待的线程
trip.signalAll();
}
这两个方法之后用得到,现在开始分析最重要的等待通过栅栏方法 await 方法:
// 不带超时机制
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
// 带超时机制,如果超时抛出 TimeoutException 异常
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
继续往里看:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 先要获取到锁,然后在 finally 中要记得释放锁
// 如果记得 Condition 部分的话,我们知道 condition 的 await() 会释放锁,被 signal() 唤醒的时候需要重新获取锁
lock.lock();
try {
final Generation g = generation;
// 检查栅栏是否被打破,如果被打破,抛出 BrokenBarrierException 异常
if (g.broken)
throw new BrokenBarrierException();
// 检查中断状态,如果中断了,抛出 InterruptedException 异常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// index 是这个 await 方法的返回值
// 注意到这里,这个是从 count 递减后得到的值
int index = --count;
// 如果等于 0,说明所有的线程都到栅栏上了,准备通过
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 如果在初始化的时候,指定了通过栅栏前需要执行的操作,在这里会得到执行
final Runnable command = barrierCommand;
if (command != null)
command.run();
// 如果 ranAction 为 true,说明执行 command.run() 的时候,没有发生异常退出的情况
ranAction = true;
// 唤醒等待的线程,然后开启新的一代
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 进到这里,说明执行指定操作的时候,发生了异常,那么需要打破栅栏
// 之前我们说了,打破栅栏意味着唤醒所有等待的线程,设置 broken 为 true,重置 count 为 parties
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 如果是最后一个线程调用 await,那么上面就返回了
// 下面的操作是给那些不是最后一个到达栅栏的线程执行的
for (;;) {
try {
// 如果带有超时机制,调用带超时的 Condition 的 await 方法等待,直到最后一个线程调用 await
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 如果到这里,说明等待的线程在 await(是 Condition 的 await)的时候被中断
if (g == generation && ! g.broken) {
// 打破栅栏
breakBarrier();
// 打破栅栏后,重新抛出这个 InterruptedException 异常给外层调用的方法
throw ie;
} else {
// 到这里,说明 g != generation, 说明新的一代已经产生,即最后一个线程 await 执行完成,
// 那么此时没有必要再抛出 InterruptedException 异常,记录下来这个中断信息即可
// 或者是栅栏已经被打破了,那么也不应该抛出 InterruptedException 异常,
// 而是之后抛出 BrokenBarrierException 异常
Thread.currentThread().interrupt();
}
}
// 唤醒后,检查栅栏是否是“破的”
if (g.broken)
throw new BrokenBarrierException();
// 这个 for 循环除了异常,就是要从这里退出了
// 我们要清楚,最后一个线程在执行完指定任务(如果有的话),会调用 nextGeneration 来开启一个新的代
// 然后释放掉锁,其他线程从 Condition 的 await 方法中得到锁并返回,然后到这里的时候,其实就会满足 g != generation 的
// 那什么时候不满足呢?barrierCommand 执行过程中抛出了异常,那么会执行打破栅栏操作,
// 设置 broken 为true,然后唤醒这些线程。这些线程会从上面的 if (g.broken) 这个分支抛 BrokenBarrierException 异常返回
// 当然,还有最后一种可能,那就是 await 超时,此种情况不会从上面的 if 分支异常返回,也不会从这里返回,会执行后面的代码
if (g != generation)
return index;
// 如果醒来发现超时了,打破栅栏,抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
好了,我想我应该讲清楚了吧,我好像几乎没有漏掉任何一行代码吧?
下面开始收尾工作。
首先,我们看看怎么得到有多少个线程到了栅栏上,处于等待状态:
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
判断一个栅栏是否被打破了,这个很简单,直接看 broken 的值即可:
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
前面我们在说 await 的时候也几乎说清楚了,什么时候栅栏会被打破,总结如下:
- 中断,我们说了,如果某个等待的线程发生了中断,那么会打破栅栏,同时抛出 InterruptedException 异常;
- 超时,打破栅栏,同时抛出 TimeoutException 异常;
- 指定执行的操作抛出了异常,这个我们前面也说过。
最后,我们来看看怎么重置一个栅栏:
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?
首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。
怎么样,CyclicBarrier 源码很简单吧。
Semaphore
有了 CountDownLatch 的基础后,分析 Semaphore 会简单很多。Semaphore 是什么呢?它类似一个资源池(读者可以类比线程池),每个线程需要调用 acquire() 方法获取资源,然后才能执行,执行完后,需要 release 资源,让给其他的线程用。
大概大家也可以猜到,Semaphore 其实也是 AQS 中共享锁的使用,因为每个线程共享一个池嘛。
套路解读:创建 Semaphore 实例的时候,需要一个参数 permits,这个基本上可以确定是设置给 AQS 的 state 的,然后每个线程调用 acquire 的时候,执行 state = state - 1,release 的时候执行 state = state + 1,当然,acquire 的时候,如果 state = 0,说明没有资源了,需要等待其他线程 release。
构造方法:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
这里和 ReentrantLock 类似,用了公平策略和非公平策略。
看 acquire 方法:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
这几个方法也是老套路了,大家基本都懂了吧,这边多了两个可以传参的 acquire 方法,不过大家也都懂的吧,如果我们需要一次获取超过一个的资源,会用得着这个的。
我们接下来看不抛出 InterruptedException 异常的 acquireUninterruptibly() 方法吧:
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
前面说了,Semaphore 分公平策略和非公平策略,我们对比一下两个 tryAcquireShared 方法:
// 公平策略:
protected int tryAcquireShared(int acquires) {
for (;;) {
// 区别就在于是不是会先判断是否有线程在排队,然后才进行 CAS 减操作
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 非公平策略:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
也是老套路了,所以从源码分析角度的话,我们其实不太需要关心是不是公平策略还是非公平策略,它们的区别往往就那么一两行。
我们再回到 acquireShared 方法,
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
由于 tryAcquireShared(arg) 返回小于 0 的时候,说明 state 已经小于 0 了(没资源了),此时 acquire 不能立马拿到资源,需要进入到阻塞队列等待,虽然贴了很多代码,不在乎多这点了:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个方法我就不介绍了,线程挂起后等待有资源被 release 出来。接下来,我们就要看 release 的方法了:
// 任务介绍,释放一个资源
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
// 溢出,当然,我们一般也不会用这么大的数
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
tryReleaseShared 方法总是会返回 true,然后是 doReleaseShared,这个也是我们熟悉的方法了,我就贴下代码,不分析了,这个方法用于唤醒所有的等待线程:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
Semphore 的源码确实很简单,基本上都是分析过的老代码的组合使用了。
总结
写到这里,终于把 AbstractQueuedSynchronizer 基本上说完了,对于 Java 并发,Doug Lea 真的是神一样的存在。日后我们还会接触到很多 Doug Lea 的代码,希望我们大家都可以朝着大神的方向不断打磨自己的技术,少一些高大上的架构,多一些实实在在的优秀代码吧。
(全文完)
Atomic
高并发的情况下,i++
无法保证原子性,往往会出现问题,所以引入AtomicInteger
类。
AtomicInteger基础使用
线程不安全示例
我们分别累加普通变量、volatile变量、AtomicInteger变量
public class TestAtomicInteger {
private static final int THREADS_COUNT = 2;
public static int count = 0;
public static volatile int countVolatile = 0;
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void increase() {
count++;//普通变量
countVolatile++;//volatile变量
atomicInteger.incrementAndGet();//AtomicInteger变量
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i< threads.length; i++) {
threads[i] = new Thread(() -> {
for(int i1 = 0; i1 < 1000; i1++) {
increase();
}
countDownLatch.countDown();
});
threads[i].start();
}
// 等线程全部算完再输出
countDownLatch.await();
// 正确值2000
System.out.println(count);//普通变量 输出1977
System.out.println(countVolatile);//volatile变量 输出1990
System.out.println(atomicInteger.get());//AtomicInteger变量 输出2000
}
}
AtomicInteger源码
- 类内部维护一个Unsafe对象,改变值通过Unsafe的CAS方法保证
package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 使用Unsafe类的CAS去更新值:Unsafe.compareAndSwapInt
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
// 被volatile修饰 // 保证可见性和不可重排序
private volatile int value;
// 类加载时运行
static {
try {
// 通过Unsafe计算出AtomicInteger类的value属性在对象中的偏移,该偏移值下边会用到
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {//初始值为0
}
public final int get() {
return value;
}
public final void set(int newValue) {
value = newValue;
}
/**
1.首先set()是对volatile变量的一个写操作, 我们知道volatile的write为了保证对其他线程的可见性会追加以下两个Fence(内存屏障)
1)StoreStore // 在intel cpu中, 不存在[写写]重排序, 这个可以直接省略了
2)StoreLoad // 这个是所有内存屏障里最耗性能的
注: 内存屏障相关参考Doug Lea大大的cookbook (http://g.oswego.edu/dl/jmm/cookbook.html)
2.Doug Lea大大又说了, lazySet()省去了StoreLoad屏障, 只留下StoreStore
*/
// 调用unsafe.putOrderedInt
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
// 设置新值,并返回旧值
public final int getAndSet(int newValue) {
// 用unsafe类进行操作
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @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.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* <p><a href="package-summary.html#weakCompareAndSet">May fail
* spuriously and does not provide ordering guarantees</a>, so is
* only rarely an appropriate alternative to {@code compareAndSet}.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful
*/
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/*
采用CAS机制,不断使用compareAndSwapInt尝试修改该值,如果失败,重新获取。如果并发量小,问题不大。
并发量大的情况下,由于真正更新成功的线程占少数,容易导致循环次数过多,浪费时间。
由于需要保证变量真正的共享,缓存行失效,缓存一致性开销变大。
底层开销可能较大,这个我就不追究了。
该函数做的事较多,不仅增加value,同时还给出返回值,返回值换成void就好了。
*/
public final int getAndIncrement() {//返回旧值
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/*
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {//从主内存拿最新的值赋值给v5
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
*/
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int incrementAndGet() {//返回新值
// this是调用的变量,valueOffset主内存的数的地址,1是+1
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
*/
// 该方法需要实现IntUnaryOperator接口,然后会调用applyAsInt方法对当前值进行处理,将当前值替换为applyAsInt方法的返回值。
public final int getAndUpdate(IntUnaryOperator updateFunction) {//返回旧值
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the updated value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
*/
public final int updateAndGet(IntUnaryOperator updateFunction) {//返回新值
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the previous value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
*/
// 返回旧值
public final int getAndAccumulate(int x,//更新值
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the updated value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the updated value
*/
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
public String toString() {
return Integer.toString(get());
}
public int intValue() {
return get();
}
public long longValue() {
return (long)get();
}
public float floatValue() {
return (float)get();
}
public double doubleValue() {
return (double)get();
}
}
getAndAddInt
先去内存拿最新的地址,然后不断进行CAS直到成功
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {//从主内存拿最新的值赋值给v5
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//compareAndSwapInt是原子性的操作:比较和替换是连着的,不可分割,不会分割成比较好后被别的线程插入更改
return var5;
}
其他Atomic
AtomicReference
AtomicReference类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。 AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。
是整体更改引用,而不是改变对象内部的值
public class UseAtomicReference {
// 保证User的原子性
static AtomicReference<User> userRef = new AtomicReference<User>();
public static void main(String[] args) {
User zhangsan = new User("zhangsan", 15);
userRef.set(zhangsan);//设置
User lisi = new User("lisi", 11);
// 不是更新的zhangsan本身
userRef.compareAndSet(zhangsan,lisi);//更改
System.out.println(userRef.get().getName());//lisi
System.out.println(userRef.get().getAge());
System.out.println(zhangsan.name);//zhangsan
System.out.println(zhangsan.age);
}
}
class User{
public User(String name, int age) {
this.name = name;
this.age = age;
}
String name;
int age;
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
atomic
处理CAS的ABA问题有两个可用的类
- AtomicMarkableReference: 用boolean标记有没有人动过
- AtomicStampedReference: 被动过几次
AtomicStampedReference
解决CAS的ABA问题
在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。
AtomicStampedReference它内部不仅维护了对象值,还维护了一个时间戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
package a;
// 处理CAS的ABA问题有两个可用的类
// AtomicMarkableReference boolean有没有人动过
// AtomicStampedReference 被动过几次
import java.util.concurrent.atomic.AtomicStampedReference;
public class TestAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference<>("zhangsan", 0); //0是版本号
public static void main(String[] args) throws InterruptedException {
String oldReference = asr.getReference(); // 初始的原值
int oldStamp = asr.getStamp(); // 初始的版本号
System.out.println("初始值" + oldReference + "======版本号" + oldStamp);
Thread lisi =
new Thread(
new Runnable() {
@Override
public void run() {
System.out.println(
Thread.currentThread().getName()
+ " lisi线程 cas执行前 以为当前变量值:"
+ oldReference
+ " 以为当前版本号"
+ oldStamp
+ " 尝试cas:"
+ asr.compareAndSet(oldReference, "lisi", oldStamp, oldStamp + 1)
+ " cas后当前值为"
+ asr.getReference()
+ " 版本号为"
+ asr.getStamp());
}
});
Thread wangwu =
new Thread(
new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(
Thread.currentThread().getName()
+ " wangwu线程 cas执行前 以为当前变量值:"
+ oldReference
+ " 以为当前版本号"
+ oldStamp
+ " 尝试cas:"
+ asr.compareAndSet(oldReference, "wangwu", oldStamp, oldStamp + 1)
+ " cas后当前值为"
+ asr.getReference()
+ " 版本号为"
+ asr.getStamp());
}
});
// 让lisi先执行,且用join确保lisi执行完了wangwu再执行
lisi.start();
lisi.join();
wangwu.start();
wangwu.join();
System.out.println("最后值和版本号" + asr.getReference() + "======" + asr.getStamp());
}
}
/*
初始值zhangsan======版本号0
Thread-0 lisi线程 cas执行前 以为当前变量值:zhangsan 以为当前版本号0 尝试cas:true cas后当前值为lisi 版本号为1
Thread-1 wangwu线程 cas执行前 以为当前变量值:zhangsan 以为当前版本号0 尝试cas:false cas后当前值为lisi 版本号为1
最后值和版本号lisi======1
*/
AtomicIntegerArray
package a;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class TestAtomicArray {
static int[] arr = new int[] {1, 2};
static AtomicIntegerArray ai = new AtomicIntegerArray(arr);
public static void main(String[] args) {
// 把索引为1的值改为3
ai.getAndSet(0, 3);
System.out.println(ai.get(0));//3
System.out.println(arr[0]);//1
}
}
参考
https://mp.weixin.qq.com/s/-MXuwOEaupFyh_2yylEZoA
https://blog.csdn.net/zhousenshan/article/details/77815022
https://blog.csdn.net/TJtulong/article/details/105345940
视频:https://www.bilibili.com/video/BV19J411Q7R5
视频文档:https://blog.csdn.net/hskw444273663/article/details/103018276