深入分析AQS实现原理

一、什么是 AQS

  AQS 全程为 AbstractQueuedSynchronizer,它提供了一个 FIFO 队列,可以看成是一个用来实现同步锁及其它涉及到同步功能的核心组件,常见的有,ReentrantLock、CountDownLatch等

  AQS是一个抽象类,主要通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提高自定义的同步组件。

  可以这么说,只要搞懂了 AQS,那么 J.U.C 中绝大部分的 API 都能轻松掌握。

1、AQS 的两种功能

从使用层面来讲,AQS 的功能分为两种:

  • 独占锁:每次只能由一个线程持有锁。 ReentrantLock 就是以独占方式实现的互斥锁;
  • 共享锁:允许多个线程同时获取锁,并发访问共享资源。比如 ReentrantReadWriteLock;

2、AQS 的内部实现

2.1 AQS 内部变量
变量名说明关注层级
state同步状态,标识当前状态是锁定、还是非锁定重点
head指向 Node 节点,同步队列、等待队列的头节点指针重点
tail指向 Node 节点,同步队列、等待队列的尾节点指针重点
unsafeUnsafe 类实例,实现线程 park、unpark 的关键知道
stateOffsetstate 字段在实例中的便宜量,用于 CAS 操作了解
headOffsethead 字段在实例中的便宜量,用于 CAS 操作了解
tailOffsettail 字段在实例中的便宜量,用于 CAS 操作了解
nextOffsetnext 字段在实例中的便宜量,用于 CAS 操作了解
2.2 AQS 工作原理介绍

  AQS 的实现依赖内部的 FIFO 的双向队列,如果当前线程竞争锁失败,那么AQS 会把当前线程以及等待状态信息构造成一个 Node 加入到这个队列中,同时调用 Unsafe 方法,使当前线程进入阻塞状态。当获取锁的线程释放了锁后,会从队列中唤醒下一个阻塞的节点(线程)

在这里插入图片描述

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点,所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。线程抢占的方式是通过 CAS 操作修改state的值,修改成功意味着当前线程抢占到了资源,修改失败的线程则加入 FIFO 队列,等待被唤醒。

Node 类组成如下

static final class Node {
	static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    /**该节点由于超时或中断而被取消,当前节点的线程作废*/
    static final int CANCELLED =  1;
    /**表明当前节点的下一节点的线程将要进入阻塞状态,需要被 unparking 唤醒*/
    static final int SIGNAL    = -1;
    /**表明当前节点的线程 处于等待状态,也就是再等待队列,等待被通知*/
    static final int CONDITION = -2;
    
    static final int PROPAGATE = -3;
	/**CANCELLED、SIGNAL、CONDITION、PROPAGATE*/
    volatile int waitStatus;
    
    volatile Node prev; //前驱节点
    volatile Node next; //后继节点
    volatile Thread thread;//当前线程
    Node nextWaiter; //存储在condition队列中的后继节点
    
    // 是否为共享锁
    final boolean isShared() { 
    	return nextWaiter == SHARED;
    }

	final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

	Node() {// Used to establish initial head or SHARED marker
    }
    
    // 将线程构造成一个Node,添加到等待队列
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 这个方法会在Condition队列使用
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

3、锁竞争、锁释放对队列的变化

3.1 锁竞争

当出现锁竞争时,这里会出现2种变化。

  1. 新的线程没有抢到锁,加入 FIFO 队列。

在这里插入图片描述
  线程 Thread0 抢占到了锁,线程 Thread1、Thread2、Thread3 抢占锁失败,进入 FIFO 同步队列,等待被唤醒。

3.2 锁释放

  当锁释放的时候,会判断 head 节点的 waitStatus 的状态是不是 != 0,如果是,则说明 FIFO 队列有等待线程,唤醒Node1节点 保存的线程(thread1)。

  1. thread1 抢到了锁,节点变化如下。

在这里插入图片描述
  threa0 释放了锁,判断 head 的 waitStatus = -1,则调用 unpack 唤醒第一个 node,也就是 thread1,这里 thread1 抢占到了锁

  1. thread1未抢到锁,节点变化如下。
    在这里插入图片描述
      threa0 释放了锁,判断 head 的 waitStatus = -1,则调用 unpack 唤醒第一个 node,也就是 thread1,这里 thread1 抢占到了锁失败,被新线程 thread3 抢占锁成功,那么 thread1 会重新进入 wait 状态,在 FIFO 同步队列中的位置是不变的。

二、AQS同步队列源码分析

  清楚了AQS的基本架构以后,我们来分析一下 AQS 的源码,以 ReentrantLock 为模型。

2.1 抢占锁

ReentrantLock.lock
public void lock() {
    sync.lock();
}

  这个是获取锁的入口,调用sync这个类里面的方法,sync是什么呢?

abstract static class Sync extends AbstractQueuedSynchronizer

  sync是 ReentrantLock 的一个静态内部类,它继承了 AQS 这个抽象类,前面说过 AQS 是一个同步工具,主要用来实现同步控制。我们在利用这个工具的时候,会继承它来实现同步控制功能。

  通过进一步分析,发现 Sync 这个类有两个具体的实现,分别是 NofairSync(非公平锁),FailSync(公平锁)。

/** 公平锁 */
static final class FairSync extends Sync 

/** 非公平锁 */
static final class NonfairSync extends Sync
  • 公平锁:表示所有线程严格按照 FIFO 队列来获取锁
  • 非公平锁:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁

公平锁和非公平锁的实现上的差异,会在文章后面做一个分析,接下来的分析仍然以非公平锁作为主要分析逻辑。

NonfairSync.lock
final void lock() {
    /**
    通过 cas 操作来修改 state 状态,表示争抢锁的操作
    */
    if (compareAndSetState(0, 1)) 
        /**
         获取锁成功
         设置当前获得锁状态的线程,当前线程赋值给 
         AQS的exclusiveOwnerThread变量,目的为了实现重入
        */
    	setExclusiveOwnerThread(Thread.currentThread()); 
    else
        // 获取锁失败
    	acquire(1); 
}

  由于 NonfairSync 继承了 Sync 继承了 AQS,其实是调用 AQS.compareAndSetState 方法

这段代码简单解释一下:

  由于这里是非公平锁,所以调用 lock 方法后,先去通过 cas 去抢占锁;而公平锁是不会先去抢占锁的,而是直接加入 FIFO 同步队列尾

  • 如果抢占锁成功,保存获得锁成功的当前线程
  • 抢占锁失败,调用 acquire 方法来走锁竞争逻辑
AQS.compareAndSetState

  对外提供,原子操作修改 AQS 的 state 状态方法。

protected final boolean compareAndSetState(int expect, int update) {
	// 调用 CAS 的原子操作,修改 AQS 的 state状态
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

state

  • 当 state=0 时,表示无锁状态
  • 当 state>0 时,表示已经有线程获得了锁,也就是state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
	......

	private volatile int state;

	......
	
}

  需要注意的是:不同的 AQS 实现,state 所表达的含义是不一样的。

Unsafe
  Unsafe类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如Netty、Hadoop、Kafka等;Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

  这个是一个 native 方法, 第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的headOffset的值),第三个参数为期待的值,第四个为更新后的值
  整个方法是原子操作,作用是如果当前时刻的值等于预期值var4相等,则更新为新的期望值 var5,如果更新成功,则返回true,否则返回false;

AQS.acquire

  如果 CAS 操作未能成功,说明 state 已经不为0,锁被其它线程持有,则走锁竞争的逻辑。(可以看出核心逻辑 cas 抢占锁、acquire 竞争锁都是 AQS 实现的)

public final void acquire(int arg) {
	if (!tryAcquire(arg) && 
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法的主要逻辑是:

  1. 通过 tryAcquire 尝试获取锁,如果成功返回 true,失败返回 false
  2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 同步队列尾部,注意这步只是加入,node 的 waitstatus 还没修改,线程也还没进入阻塞状态
  3. acquireQueued,将Node作为参数,通过自旋去尝试获取锁,获取失败则设置 Node 节点 waitstatus 的状态。
NonfairSync.tryAcquire

  AQS 将 tryAcquire 的逻辑交给实现类自己实现,那么各个实现类就可以玩出各种花样。而 NonfairSync 实现了:重试获取锁 + 非公平锁 + 可重入锁的特性。而 FairSync 实现了:公平锁的特性

protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}
Sync.nonfairTryAcquire
ffinal boolean nonfairTryAcquire(int acquires) {
    //获得当前执行的线程
    final Thread current = Thread.currentThread();
    //获得state的值
    int c = getState(); 
    //state=0说明当前是无锁状态
    if (c == 0) { 
        /**
          通过cas操作来替换state的值改为1,大家想想为什么要用cas呢?
          理由是,在多线程环境中,直接修改 state=1 会存在线程安全问题,你猜到了吗?
        */
        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");
        // 重新设置state = state + 1
        setState(nextc);
        return true;
    }
    return false;
}

该方法的主要逻辑:

  1. 获取当前线程,判断当前的锁的状态
  2. 如果 state = 0 表示当前是无锁状态,通过 cas 更新 state 状态的值,更新成功表面获取到了锁;更新失败继续往下执行。
  3. 如果当前线程是属于重入,则增加重入次数
AQS.addWaiter

  当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node,然后添加到 FIFQ 队列,只是添加到队列。

private Node addWaiter(Node mode) { //mode=Node.EXCLUSIVE 独占锁
	//将当前线程封装成Node
    Node node = new Node(Thread.currentThread(), mode); 
    
    // tail是AQS的中表示同步队列队尾的属性,刚开始为 null,所以进行enq(node)方法
    Node pred = tail;
    //tail不为空的情况
    if (pred != null) { 
        //将当前线程的 Node 的 prev 节点指向 tail
        node.prev = pred;  
        
        // cas操作,将 AQS.tail 指向新的 node节点
        if (compareAndSetTail(pred, node)) {
            // 旧的tail 指向新的 tail节点
            pred.next = node;
            // 添加成功返回即可
            return node;
        }
    }
    /**
    	1.程序初期 tail = null
    	2.cas 添加新node 到队列尾失败
    	1.2 都进入这里
    */
    // 这里会保证最终成功添加到队列尾
    enq(node); 
   
    return node;
}

该方法的主要逻辑是:

  1. 将当前线程封装成 Node对象
  2. 判断 FIFQ 节点中的 tail 是否为空 ,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS同步队列尾
  3. 如果为空 或者 cas失败,调用 enq() 方法将节点添加到 AQS同步队列尾
AQS.enq

  enq方法就是通过自旋操作把当前节点加入到队列中

private Node enq(final Node node) {
	// 自旋,不做过多解释,最终一定能成功添加到阻塞队列尾
    for (;;) {
    	Node t = tail; 
        // 如果是第一次添加到队列,那么 tail == null
        if (t == null) { 
        	//创建一个空的Node,并通过CAS操作设置为head节点
            if (compareAndSetHead(new Node()))
            	//此时队列中只一个头结点,所以tail也指向它
                tail = head;
            
            } else {
            
            	/**
            	 tail 不为 null,进入else区域
            	仔细对比,else区逻辑和addWaiter方法添加到队列逻辑是一样的。这里会循环添加,直至添加成功。
            	*/
				
        
				// 将当前线程的 Node 的 prev 节点指向 tail
                node.prev = t;
            
            	// cas操作,将 AQS.tail 指向新的 node节点
                if (compareAndSetTail(t, node)) {
					
					// 旧的tail 指向新的 tail节点
                    t.next = node; 
                    // 添加成功返回即可
                    return t;
                }
            }
        }
    }
}

  通过 AQS.addWaiter 方法,即便有多个线程同时要插入 FIFQ 队列,最后都能插入成功。(针对这种 cpu 型计算,CAS 性能还是比较高的)

AQS.acquireQueued

  将添加到队列中的 Node 作为参数传入 acquireQueued 方法,这里面会再次做抢占锁的操作,抢占成功 or 失败都会有不同的操作,比如修改 note 节点的 waitstatus、线程阻塞、删除 node 节点等。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // for循环,要么自旋抢占锁成功,要么线程挂起
        for (;;) {
            
            // 获取 node的 prev 节点,若为 null 即刻抛出 NullPointException
            final Node p = node.predecessor();
          
            /**
              1.如果 前驱prev 为 AQS.head 才有资格进行自旋抢锁 
              2.自旋抢锁 还是调用重新后的 tryAcquire 方法
            */
            if (p == head && tryAcquire(arg)) {
                /**
                 下面逻辑主要是:如果自旋抢锁成功,
                 当前节点往上升级为 head 节点
                */
                setHead(node);
                p.next = null; 
                failed = false; 
                // 返回fasle,中断后面的逻辑处理
                return interrupted;
            }
			/**
			  1.前驱prev 不是 head节点 
			  2.自旋获取锁失败
			  1、2步骤都会走到这里,根据节点的 waitStatus
			  决定是否需要挂起线程
			*/
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 若前面为true,则执行线程挂起,待下次唤醒的时候检测中断的标志
                interrupted = true;
        }
    } finally {
        /**
        	1.成功获取锁了,调用 return 会执行到这里,但是 faile = false 不会这些下面方法
        	2.抛出异常会执行到这里,哪里会抛出异常 tryAcquire() 和 predecessor(),会执行下面方法。主要是一种容错机制,因为你可以在业务种唤醒任何线程,为了不打断整个 AQS允许,需要这么一种措施
        */
        if (failed) 
            // 将 node 从同步队列种剔除
            cancelAcquire(node);
    }
}

该方法的逻辑如下:

  1. 获取当前节点 node 的 前驱prev 节点
  2. 如果前驱prev 节点 是 head节点,那么它就有资格去争抢锁,调用重写的 tryAcquire 抢占锁
  3. 抢占锁成功以后,把当前节点设置为 AQS.head,并且移除原来的初始化head节点
  4. 前驱prev 不是 head节点 or 自旋获取锁失败,根据 waitStatus 状态做相应的操作。对于刚加入同步队列的 node,那么它的前驱prev 的 waitStatus == 0,修改为 waitStatus = -1后,返回false,注意这里为啥要返回 false,很关键。
  5. 如果前驱prev 的 waitStatus 已经是 -1了,则调用 LockSupport.park 进行线程挂起操作

重点,对于步骤4,如果 node 的前驱prev 一开始就是 head,修改 waitStatus = -1 后,返回 false,则会再走一次抢占锁的逻辑?为什么要再走一次?
  答:在抢占锁失败到执行步骤4之间,锁释放逻辑刚好在这之间被执行完,如果这时直接执行步骤5,线程会挂起将永远不会被唤醒。执行步骤4后,通过再抢占一次锁来确保 waitStatus 正确性(PS:这里的实现方式有点类似双重检验,双重检验:修改之前检验,修改之后检验)

  前面的逻辑都很好理解,主要看一下shouldParkAfterFailedAcquire这个方法和parkAndCheckInterrupt的作用

AQS.shouldParkAfterFailedAcquire

  从上面的分析可以看出,node 的同步队列第二个节点自旋失败 or 不是第二个节点的则会进行调用 shouldParkAfterFailedAcquire(p, node)操作

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 节点的 waitStatus
    int ws = pred.waitStatus; 
    
    //如果是 SIGNAL = -1状态,返回 true,线程可以开始阻塞了
    if (ws == Node.SIGNAL)
               return true;
    
	
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { 
        
        
        /**
         1.如果节点的waitStatus 为“0”或者“共享锁”状态,则设置节点为SIGNAL状态。
         2.这里线程不会立刻进入阻塞状态,返回 false 后还会再重试一次自旋获取锁的机会 
        */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
AQS.parkAndCheckInterrupt

  如果shouldParkAfterFailedAcquire 返回了 true,则会执行:parkAndCheckInterrupt()方法,它是通过 LockSupport.park(this) 将当前线程挂起到 WATING状态(注意synchronized是进入Blocked状态),它需要等待一个中断、unpark方法来唤醒它,通过这样一种 FIFO 的机制的等待,来实现了 Lock的操作。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

LockSupport
  LockSupport 类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport 实际上是调用了 Unsafe 类里的方法。在这里需要关注 Unsafe 的这2个方法:

public native void park(boolean var1, long var2);

public native void unpark(Object var1);

  unpark 函数为线程提供“许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。

  permit 相当于0/1的开关,默认是0,调用一次 unpark 就加1变成了1。调用一次 park 会消费 permit,又会变成0。 如果再调用一次park会阻塞,因为permit已经是0了。直到permit变成1,这时调用unpark会把permit设置为1,每个线程都有一个相关的permit,permit最多只有一个,重复调用unpark不会累积

  如下一些案例,稍微演示一下,会更容易理解:

public static void main(String[] args) {
     ystem.out.println(1);
	// 先唤醒,设置 peimit = 1
    LockSupport.unpark(Thread.currentThread());
  	// 阻塞, peimit = 0,peimit 可消费,线程不阻塞继续往下执行
    LockSupport.park();
        
    System.out.println(2);
}
result:
1
2

  这里可以看出,park 和 unpark 执行顺序是没影响的。

public static void main(String[] args) {
     ystem.out.println(1);
	// 先唤醒,设置 peimit = 1
    LockSupport.unpark(Thread.currentThread());
  	// 阻塞, peimit = 0,peimit 可消费,线程不阻塞继续往下执行
    LockSupport.park();
    // 阻塞, peimit = -1,线程阻塞,不往下执行    
    LockSupport.park();
    System.out.println(2);
}
result:
1
public static void main(String[] args) {
     ystem.out.println(1);
    // 先唤醒,设置 peimit = 1
    LockSupport.unpark(Thread.currentThread());
	// 先唤醒,设置 peimit = 1,并不会自增的
    LockSupport.unpark(Thread.currentThread());
  	// 阻塞, peimit = 0,peimit 可消费,线程不阻塞继续往下执行
    LockSupport.park();
    // 阻塞, peimit = -1,线程阻塞,不往下执行    
    LockSupport.park();
    System.out.println(2);
}
result:
1

2.2 释放锁

ReentrantLock.unlock

  该方法主要调用了 AQS.release(int arg) 方法

public void unlock() {
	sync.release(1);
}
AQS.release

  AQS.tryRelease 释放锁逻辑方法,里面会抛出异常,它和 AQS.tryAcquire 加锁逻辑方法一样,需要被重写;ReentrantLock.NonfairSync 实现 AQS.tryAcquire 方法有重入锁的逻辑,它同样要实现 AQS.tryRelease 方法也要有重入锁释放的逻辑

public final boolean release(int arg) {
    // tryRelease 试图去释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 判断 FIFQ 队列中是否有 waiting的线程
        if (h != null && h.waitStatus != 0)
            // 进入唤醒的逻辑
            unparkSuccessor(h);
        return true;
    }
    return false;
}

  该方法主要做了两件事:

  1. 释放锁
  2. 如果同步队列有 waiting 的线程,则唤醒挂起的线程
Sync.tryRelease
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
    		// 检验
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
    		
    		/**
    		 1.如果 c = 0,线程释放锁,
    		 2.如果 c != 0,线程不释放锁,state - 1,这里就是重入锁的判断
    		*/
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
AQS.unparkSuccessor

  在释放锁后,如果 FIFQ 队列中还有waiting线程,则需要唤醒。

private void unparkSuccessor(Node node) {
    // 获取 当前节点 也就是 head 节点的 waitStatus
    int ws = node.waitStatus;
    
    if (ws < 0)
        // 小于0,表示后驱节点的线程阻塞,cas操作修改 当前节点 Node waitStatus = 0
        compareAndSetWaitStatus(node, ws, 0);
    
    // 获取当前节点node 的后驱节点
    Node s = node.next;
    // 判断head的后继节点是否为空 或者 是否是取消状态,
    if (s == null || s.waitStatus > 0) { 
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
        /* 
         * 从尾反向遍历,主要是因为head后驱节点是 null or 是取消状态,我们应该唤醒head后驱节点的下一个节点,
         * 那如果直接 s.next获取就可以了不是吗?错了,在doAcquireInterruptibly.cancelAcquire方法,
         * 它将 node.next = node 设置为自己    
         */            
            if (t.waitStatus <= 0) 
                s = t;
    }
    
    /**
     如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法
     来释放对应的被挂起的线程,
     这样一来将会有一个节点唤醒后继续进入循环进一步尝试 tryAcquire()方法来获取锁		
    */
    if (s != null)
        LockSupport.unpark(s.thread); 
}

三、AQS 等待队列源码分析

  AQS 的内部类 ConditionObject 主要是为并发编程中的同步提供了等待通知的实现方式,可以在不满足某个条件的时候挂起线程等待。直到满足某个条件的时候在唤醒线程。

  这里也以 ReentrantLock 为模型,使用方法如下:

public class AQSTest {

    public static ReentrantLock lock = new ReentrantLock();

    public static Condition condition = lock.newCondition();
	// 消费者
    public static Thread thread = new Thread() {
        @Override
        public void run() {

            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 进入瓜地等待西瓜成熟");
                condition.await();
                System.out.println(Thread.currentThread().getName() + " 被唤醒去摘西瓜");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 摘了西瓜");

            lock.unlock();
        }
    };
	// 生产者
    public static Thread thread1 = new Thread() {
        @Override
        public void run() {
            lock.lock();
            condition.signal();
            System.out.println(Thread.currentThread().getName() + " 瓜地生产了西瓜");
            lock.unlock();
        }
    };


    public static void main(String[] args) {
        thread.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.start();
    }
}

  以上生产者、消费者模式是不是很眼熟。对,Condition 主要是为了配合 ReentrantLock 使用,类似于 wait 和 notify 配合 synchronize 使用一样,起到线程之间的通信作用。只不过 Condition 作用更为强大,在一个 AQS同步器中,可以定义多个 Condition。

  AQS 的内部类 ConditionObject 主要提供了这三个方法来实现,线程之间的通信:

public class ConditionObject implements Condition, java.io.Serializable {

	// 当前线程添加 等待队列尾,释放锁,然后进入 waiting 状态
	public final void await() throws InterruptedException{};

	public final void signalAll(){};
	// 将等待队列第一个节点,加入到同步队列,等待被唤醒
	public final void signal(){};

}

  等待队列也是一个 FIFO 的队列,跟同步队列一样,节点也是 AbstractQueuedSynchronizer.Node类。

3.1 进入等待队列,释放锁资源

AQS$ConditionObject.await

  AQS内部类ConditionObject提供的等待方法await

public final void await() throws InterruptedException {
    		// 判断当前线程是否设置中断标识
            if (Thread.interrupted())
                throw new InterruptedException();
    		// 生成Node节点,并插入到 FIFO 等待队列尾
            Node node = addConditionWaiter();
    			
    		/**
    		由于调用await()方法的线程是已经获取了锁的,所以在加入到等待队列之后,
    		需要去释放锁,并唤醒同步队列下一个阻塞线程
    		*/
            int savedState = fullyRelease(node);
    
            int interruptMode = 0;
    		
    		/**
    		 判断当前node 在不在同步队列,如果不在进行线程阻塞
    		 如果在的话,说明在释放锁后,被其它线程获取锁执行了 signal 方法,
    		 那么线程没必须进入阻塞状态了(这里要结合释放锁的代码看)
    		*/
            while (!isOnSyncQueue(node)) {
                // 当前线程挂起
                LockSupport.park(this);
                // 当这个线程被唤醒后,会继续往下执行
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            } 
    		// 熟悉的 acquireQueued 方法,上文有解析  
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

  这里的逻辑一定是先添加到等待队列,再释放锁资源,再看线程要不要阻塞挂起

这个方法的主要逻辑:

  1. 将当前线程封装成 node,添加到 FIFO 等待队列尾
  2. 释放当前线程的锁资源
  3. 判断线程是否要挂起等待被唤醒
  4. 线程被唤醒执行 acquireQueued 获取锁的方法
AQS$ConditionObject.addConditionWaiter

  该方法主要是生成 Node节点,并插入到 FIFO 等待队列尾

private Node addConditionWaiter() {
            Node t = lastWaiter;
    		// 如果尾节点 != null 且 尾节点 不是 条件等待状态
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 从头节点开始清除 不是 条件等待状态的节点
                unlinkCancelledWaiters();
                // 重新给t 赋值 新的尾节点
                t = lastWaiter;
            }
    		// 将当前线程 封装成 node
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
    		// 将 node插入等待队列队尾
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
AQS.fullyRelease

  该方法获取当前 state 的值,调用 AQS.release,释放锁资源,唤醒同步队列的下一个节点线程

final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // 获取state的值
            int savedState = getState();
            // 这里会调用AQS.release,上面有讲过,释放锁资源,唤醒同步队列的下一个节点线程
            if (release(savedState)) {
                failed = false;
                
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

3.2 唤醒线程

AQS$ConditionObject.signal

  AQS内部类 ConditionObject 提供的通知方法 signal

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                // 取等待队列 第一个节点进行唤醒
                doSignal(first);
}
AQS$ConditionObject.doSignal
private void doSignal(Node first) {
            do {
                // 这部分代码是将第一个 node节点 从等待队列剔除
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
                
                // 从first开始遍历等待队列,把第一个非空、不是等待状态的node节点transfer到同步队列
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

该方法的主要逻辑:

  1. 剔除等待队列 的第一个节点
  2. 将第一个节点转移到 同步队列
AQS.transferForSignal
final boolean transferForSignal(Node node) {
        
    	// CAS操作 将节点 从 -2等待状态 改为 0无锁状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        // 这里调用AQS.enq方法,之前有讲过,通过自旋操作把当前节点加入到队列中。
    	// 这里返回的p:同步队列中,node节点的 前驱prev节点。
        Node p = enq(node);
        int ws = p.waitStatus;
    	
    	// ws > 0,如果node的前驱节点是取消状态,直接唤醒 node的等待线程
        // 如果 node的前驱节点不是取消状态,则修改前驱节点的状态为 等待触发状态-1,不进行唤醒。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

  该方法的主要逻辑:

  1. 把等待队列剔除的 node节点 状态从 -2 修改为 0
  2. 将 node节点 添加到 同步队列
  3. 判断 node的前驱节点是否取消状态,如果是,直接唤醒 node的等待线程;如果不是,修改 node的前驱节点为等待触发状态-1。

  这里有个问题,等待队列剔除的node,添加到同步队列后,怎么唤醒呢?

  结合上文讲的同步队列,在调用 AQS.release 后会释放锁资源,唤醒同步队列下一个阻塞线程,也就是在调用 ReentrantLock.unlock 时。

3.3 AQS 通信流程图:

在这里插入图片描述
  初始状态,thread0 抢占了锁,thread1 抢锁失败,生成 Node 节点加入 FIFO 同步队列。

在这里插入图片描述
  thread0 执行业务逻辑,调用了 await() 方法。thread0 生成 Node 节点进入 FIFO 等待队列,然后释放锁,释放锁会唤醒同步队列第一个节点,也就是 thread1 被唤醒,thead1 被唤醒后抢到了锁,同步节点进行变更
在这里插入图片描述
  thread1 执行业务逻辑,调用了 sigin() 方法。将等待队列第一个 Node 转移到 同步队列,也就是 thread0 所在的 Node 转移到同步队列
在这里插入图片描述
  thread1 释放了锁,唤醒同步队列第一个节点,也就是 thread0 被唤醒,thread1 抢到了锁。至此整个 await()、signal() 通信完成。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值