线程安全性原理分析---Lock(让你明白彻底AQS实现原理)

7 篇文章 0 订阅
6 篇文章 0 订阅
ReentrantLock 的实现原理

结合上篇《Lock基础分析》那么我们接下来分析下它的实现原理。我们知道锁的基本原理是:基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。那么在 ReentrantLock 中,也一定会存在这样的问题。就是在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?

AQS 的引入

在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具,也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。

AQS两种特性

从使用层面来说,AQS 的功能分为两种:独占和共享,
独占锁,每次只能有一个线程持有锁,比如前面给大家演示的 ReentrantLock 就是以独占方式实现的互斥锁。
共 享 锁 ,允 许 多 个 线 程 同 时 获 取 锁 ,并 发 访 问 共 享 资 源 , 比 如ReentrantReadWriteLock

AQS 的内部实现

AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向当前节点的后继节点和当前节点前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
在这里插入图片描述
Node 节点的组成

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /** waitStatus value to indicate the next acquireShared should  unconditionally propagate */
        static final int PROPAGATE = -3;
             
        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(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;
        }
    }
释放锁以及添加线程对于队列的变化过程:

—出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景:
在这里插入图片描述
里会涉及到两个变化

  1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己。
  2. 通过CAS 将tail 重新指向新的尾部节点
    代码实现如下:
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); //将线程封装成node 节点
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;  //将 tail 先赋值给 pred
        if (pred != null) {
            node.prev = pred;  //设置新加入节点的prev 节点为之前的未节点
            if (compareAndSetTail(pred, node)) { //这里考虑多个线程相互竞争的问题因此通过CAS 将tail 重新指向新的尾部节点
                pred.next = node; //修改当前节点的前置节点的 next 节点指向自己
                return node;
            }
        }
        enq(node);
        return node;
    }

—head 节点表示获取锁成功的节点,当head结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:
在这里插入图片描述
这个过程也是涉及到两个变化

  1. 修改 head 节点指向下一个获得锁的节点

  2. 新的获得锁的节点,将 prev 的指针指向 null

    说明:设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可
    源码如下:

 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)) {  //前驱节点为 head 头节点,并且当前线程获取锁成功
                    setHead(node);   //设置当前节点为头节点
                    p.next = null; // help GC 前置节点的next 设置为null 从AQS链表中断开
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
ReentrantLock 的源码分析

以 ReentrantLock 作为切入点,来看看在这个场景中是如何使用 AQS 来实现线程的同步的,首先我们从它的切入点 lock() 方法调用来入手,时序图如下:
在这里插入图片描述 ReentrantLock.lock()这个是 reentrantLock 获取锁的入口

 public void lock() {
        sync.lock(); //入口
    }

sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。
在这里插入图片描述
我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,Sync 会继承 AQS 来实现对不同的应场景。
Sync 有两个具体的实现类,分别是:
在这里插入图片描述
NofairSync:非公平队列,表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
FailSync: 公平队列,表示所有线程严格按照 FIFO 来获取锁
NofairSync .lock
以非公平锁为例,来看看 lock 中的实现

  1. 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有没有线程排队,我先上来 cas 去抢占一下。
  2. CAS 成功,就表示成功获得了锁
  3. CAS 失败,调用 acquire(1)走锁竞争逻辑
 static final class NonfairSync extends Sync { 
        final void lock() {
            if (compareAndSetState(0, 1))  //上来先通过CAS 抢占下锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1); //锁竞争逻辑
        }
    }

CAS 的实现原理

protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

通过 CAS 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false.
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。
–state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
在这里插入图片描述
在这里插入图片描述

  1. 当 state=0 时,表示无锁状态
  2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。 而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁。
    Unsafe 类
    Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等;
    Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障等等。
    而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个参数为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;
    一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移,用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置,所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量。
    compareAndSwapInt
    在 unsafe.cpp 文件中,可以找到 compareAndSwarpInt 的实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);  //将 Java 对象解析成 JVM 的 oop(普通对象指针),
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p和地址偏移量找到地址
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;  //基于 cas 比较并替换, x 表示需要更新的值,addr 表示 state 在内存中的地址,e 表示预期值
UNSAFE_END

AQS .accquire
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续 acquire(1)操作,源码如下:

 final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
  • 大家思考一下,acquire 方法中的 1 的参数是用来做什么呢?
  1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
  2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
  3. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  //通过 tryAcquire 尝试获取独占锁,
           acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
           selfInterrupt();
    }

NonfairSync.tryAcquire
这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false。
它是重写 AQS 类中的 tryAcquire 方法,并且大家仔细看一下 AQS 中 tryAcquire方法的定义,并没有实现,而是抛出异常。
AQS 中 tryAcquire方法:

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成 abstract,让子类来实现,大家想想为什么这块为什么抛出一个异常这样设计?

ReentrantLock.nofairTryAcquire
1.获取当前线程,判断当前的锁的状态

  • 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值
  • 当前线程是属于重入,则增加重入次数
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); //获取当前线程
            int c = getState(); // 当前状态
            if (c == 0) {  //表示无锁状态
                if (compareAndSetState(0, acquires)) {  cas 替换 state 的值,cas 成功表示获取锁成功
                    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;
        }

AQS.addWaiter()
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node,入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能。

  • 将当前线程封装成 Node
  • 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
  • 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); //将当前线程封装成 Node
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {  //当前链表中的 tail 节点是否为空,
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {//如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
                pred.next = node; //设置成功以后,把原 tail 节点的 next指向当前 node
                return node;
            }
        }
        enq(node); //把 node 添加到同步CAS队列
        return node;
    }

图解分析
假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter方法结束后,AQS 中的链表变化过程图:
在这里插入图片描述
AQS.acquireQueued()
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁。
1.获取当前节点的 prev 节点
2.如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
3.抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
4.如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
5.最后,通过 cancelAcquire 取消获得锁的操作

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); //获取当前节点的 prev 节点
             if (p == head && tryAcquire(arg)) { //如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢锁
                    setHead(node); //抢占锁成功以后,把获得锁的节点设置为 head
                    p.next = null; // help GC  并且移除原来的初始化 head节点
                    failed = false; 
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&  //则根据 waitStatus 决定是否需要挂起线程
                    parkAndCheckInterrupt()) 
                    interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
            }
        } finally {
            if (failed)
                cancelAcquire(node);//通过 cancelAcquire 取消获得锁的操作     
        }
    }

NofairSync.tryAcquire
这个方法在前面分析过,就是通过 state 的状态来判断是否处于无锁状态,然后在通过 cas 进行竞争锁操作。成功表示获得锁,失败表示获得锁失败
shouldParkAfterFailedAcquire
如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire 方法
Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)

  • CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus
    为CANCELLED,即结束状态,进入该状态后的结点将不会再变化
  • SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
  • CONDITION: 和 Condition 有关系,后续会讲解
  • PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态
  • 0:初始状态
    这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。
  1. 如果 ThreadB 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
  2. 通过循环扫描链表把 CANCELLED 状态的节点移除
  3. 修改 pred 节点的状态为 SIGNAL,返回 false.
    返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt 挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)  //如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放,
            return true;  
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);  //ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //利用 cas 设置 prev 节点的状态为 SIGNAL(-1)
        }
        return false;
    }

parkAndCheckInterrupt()
使用 LockSupport.park 挂起当前线程编程 WATING 状态
Thread.interrupted(),返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在 acquire 方法中会执selfInterrupt()。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//jdk 1.6之后提供的。
        return Thread.interrupted();  //返回当前线程是否被其他线程触发过中断请求
    }

selfInterrupt(): 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的

 static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

图解分析
通过 acquireQueued 方法来竞争锁,如果 ThreadA 还在执行中没有释放锁的话,意味着 ThreadB 和 ThreadC 只能挂起了。
在这里插入图片描述

锁的释放流程

如果这个时候 ThreadA 释放锁了,那么我们来看锁被释放后会产生什么效果
ReentrantLock.unlock()方法
在 unlock 中,会调用 release 方法来释放锁

    public final boolean release(int arg) {
        if (tryRelease(arg)) { //释放锁成功
            Node h = head;  //得到 aqs 中 head 节点
            if (h != null && h.waitStatus != 0)
                //如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease(arg)
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会释放锁成功。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//通过将 state 状态减掉传入的参数值(参数是 1)
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//如果结果状态为 0
                free = true;
                setExclusiveOwnerThread(null);  //就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行
            }
            setState(c);
            return free;
        }

unparkSuccessor(h)释放锁

private void unparkSuccessor(Node node) {
      
        int ws = node.waitStatus;//获得 head 节点的状态
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);  // 设置 head 节点状态为 0
        Node s = node.next;//得到 head 节点的下一个节点  也就是当前节点。
        //如果下一个节点为 null 或者 status>0 表示 cancelled 状态.
        if (s == null || s.waitStatus > 0) { 
            s = null;
            //通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null) //next 节点不为空,直接唤醒这个线程即可
            LockSupport.unpark(s.thread);
    }
原本挂起的线程继续执行

通过 ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行,应该从哪里执行大家还有印象吧。 原来被挂起的线程是在 acquireQueued 方法中,所以被唤醒以后继续从这个方法开始执行
AQS .acquireQueued
这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。
由于 ThreadB 的 prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这个时候调用 tryAcquire 方法时,可以顺利获取到锁

  1. 把 ThreadB 节点当成 head
  2. 把原 head 节点的 next 节点指向为 null
 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); //把 ThreadB 节点当成 head
                    p.next = null; // help GC  把原 head 节点的 next 节点指向为 null
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

图解分析
1… 设置新 head 节点的 prev=null
2.设置原 head 节点的 next 节点为 null
在这里插入图片描述

Condition

在前面学习 synchronized 的时候,有讲到 wait/notify 的基本使用,结合synchronized 可以实现对线程的通信。那么这个时候我就在思考了,既然 J.U.C 里面提供了锁的实现机制,那 J.U.C 里面有没有提供类似的线程通信的工具呢? Condition 就应运而生了。
Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒

Condition 的基本使用
wait();

public class ConditionDemoWait implements Runnable {

    private Lock lock;
    private Condition condition;

    public ConditionDemoWait(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println("begin -ConditionDemoWait");
        try {
            lock.lock();
            condition.await();
            System.out.println("end - ConditionDemoWait");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

signal()的使用;

public class ConditionDemoSignal implements Runnable {

    private Lock lock;
    private Condition condition;

    public ConditionDemoSignal(Lock lock, Condition condition) {
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
        System.out.println("begin -ConditionDemoSignal");
        try {
            lock.lock();
            condition.signal();
            System.out.println("end - ConditionDemoSignal");
        } finally {
            lock.unlock();
        }
    }
}

通过这个案例简单实现了 wait 和 notify 的功能,当调用 await 方法后,当前线程会释放锁并等待,而其他线程调用 condition 对象的 signal 或者 signalall 方法通知并被阻塞的线程,然后自己执行 unlock 释放锁,被唤醒的线程获得之前的锁继续执行,直到最后释放锁。
所以,condition 中两个最重要的方法,一个是 await方法,一个是 signal 方法
await:把当前线程阻塞挂起
signal:唤醒阻塞的线程

Condition 源码分析

调用 Condition,需要获得 Lock 锁,所以意味着会存在一个 AQS 同步队列,先来看 Condition.await 方法。
condition .await
调用 Condition 的 await()方法(或者以 await 开头的方法),会使当前线程进入等待队列并且释放锁,同时线程状态变为等待状态。当从 await()方法返回时,当前线程一定获取了 Condition 相关联的锁。

 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter(); //创建一个新的节点,节点状态为 condition,采用的数据结构仍然是链表
            int savedState = fullyRelease(node);//释放当前的锁,得到锁的状态,并唤醒 AQS 队列中的一个线程
            int interruptMode = 0;
            
            //如果当前节点没有在同步队列上,即还没有被 signal,则将当前线程阻塞
            while (!isOnSyncQueue(node)) {//判断这个节点是否在 AQS 队列上,第一次判断的是 false,因为前面已经释放锁了
                LockSupport.park(this); // 第一次总是 park 自己,开始阻塞等待
                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);
        }

Condition .signal
调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

  public final void signal() {
            if (!isHeldExclusively()) //先判断当前线程是否获得了锁
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

Condition.doSignal

private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)  // 如果第一个节点的下一个节点是 null, 那么, 最后一个节点也是 null.
                    lastWaiter = null; // 将 next 节点设置成 null
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒

final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
// 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL
失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞),
if (ws > 0 || !compareAndSetWaitStatus(p, ws,
Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒输入节点上的线程.
return true;
}

AQS.transferForSignal
该方法先是 CAS 修改了节点状态,如果成功,就将这个节点放到 AQS 队列中,然后唤醒这个节点上的线程。此时,那个节点就会在 await 方法中苏醒。

final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
Node p = enq(node);
int ws = p.waitStatus;
// 如果上一个节点的状态被取消了, 或者尝试设置上一个节点的状态为 SIGNAL
失败了(SIGNAL 表示: 他的 next 节点需要停止阻塞),
if (ws > 0 || !compareAndSetWaitStatus(p, ws,
Node.SIGNAL))
LockSupport.unpark(node.thread); // 唤醒输入节点上的线程.
return true;
}

Condition 总结
阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入正常锁的获取流程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值