关于java:15-w字16-张图轻松入门-RLockAQS-并发编程原理

43次阅读

共计 16773 个字符,预计需要花费 42 分钟才能阅读完成。

前言

AbstractQueuedSynchronizer(AQS)是 Java 并发编程中绕不过来的一道坎,JUC 并发包下的 Lock、Semaphore、ReentrantLock 等都是基于 AQS 实现的。AQS 是一个形象的同步框架,提供了原子性治理同步状态,基于阻塞队列模型实现阻塞和唤醒期待线程的性能

文章从 ReentrantLock 加锁、解锁利用 API 动手,逐渐解说 AQS 对应源码以及相干隐含流程

列出本篇文章纲要以及相干知识点,不便大家更好的了解

什么是 ReentrantLock

ReentrantLock 翻译为 可重入锁 ,指的是一个线程可能对  临界区共享资源进行反复加锁

确保线程平安最常见的做法是利用锁机制(Lock、sychronized)来对 共享数据做互斥同步 ,这样在同一个时刻,只有  一个线程能够执行某个办法或者某个代码块 ,那么操作必然是  原子性的,线程平安的

这里就有个疑难,因为 JDK 中关键字 synchronized 也能同时反对原子性以及线程平安

有了 synchronized 关键字后为什么还须要 ReentrantLock?

为了大家更好的把握 ReentrantLock 源码,这里列出两种锁之间的区别

通过以上六个维度比对,能够看出 ReentrantLock 是要比 synchronized 灵便以及反对性能更丰盛

集体整顿了一些材料,有须要的敌人能够间接点击支付。

  • 25 大 Java 面试专题(附解析)
  • 从 0 到 1Java 学习路线和材料
  • Java 外围常识集

什么是 AQS

AQS(AbstractQueuedSynchronizer)是一个用来构建锁和同步器的形象框架,只须要继承 AQS 就能够很不便的实现咱们自定义的多线程同步器、锁

如图所示,在 java.util.concurrent 包下相干锁、同步器(罕用的有 ReentrantLock、ReadWriteLock、CountDownLatch…)都是基于 AQS 来实现

AQS 是典型的模板办法设计模式,父类(AQS)定义好骨架和外部操作细节,具体规定由子类去实现

AQS 外围原理

如果被申请的共享资源未被占用,将以后申请资源的线程设置为独占线程,并将共享资源设置为锁定状态

AQS 应用一个 Volatile 润饰的 int 类型的成员变量 State 来示意同步状态,批改同步状态胜利即为取得锁

Volatile 保障了变量在多线程之间的可见性,批改 State 值时通过 CAS 机制来保障批改的原子性

如果共享资源被占用,须要肯定的阻塞期待唤醒机制来保障锁的调配,AQS 中会将竞争共享资源失败的线程增加到一个变体的 CLH 队列中

对于撑持 AQS 个性的重要办法及属性如下:

public abstract class AbstractQueuedSynchronizer 
  extends AbstractOwnableSynchronizer implements java.io.Serializable {
   // CLH 变体队列头、尾节点
    private transient volatile Node head;
   private transient volatile Node tail;
   // AQS 同步状态
    private volatile int state;
   // CAS 形式更新 state
   protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

CLH 队列

既然是 AQS 中应用的是 CLH 变体队列,咱们先来理解下 CLH 队列是什么

CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列 。申请线程只在本地变量上自旋, 它一直轮询前驱的状态 ,如果发现  前驱节点开释了锁就完结自旋

通过对 CLH 队列的阐明,能够得出以下论断

  1. CLH 队列是一个单向链表,放弃 FIFO 先进先出的队列个性
  2. 通过 tail 尾节点(原子援用)来构建队列,总是指向最初一个节点
  3. 未取得锁节点会进行自旋,而不是切换线程状态
  4. 并发高时性能较差,因为未取得锁节点一直轮训前驱节点的状态来查看是否取得锁

AQS 中的队列是 CLH 变体的虚构双向队列,通过将每条申请共享资源的线程封装成一个节点来实现锁的调配

相比于 CLH 队列而言,AQS 中的 CLH 变体期待队列领有以下个性

  1. AQS 中队列是个双向链表,也是 FIFO 先进先出的个性
  2. 通过 Head、Tail 头尾两个节点来组成队列构造,通过 volatile 润饰保障可见性
  3. Head 指向节点为已取得锁的节点,是一个虚构节点,节点自身不持有具体线程
  4. 获取不到同步状态,会将节点进行自旋获取锁,自旋肯定次数失败后会将线程阻塞,绝对于 CLH 队列性能较好

意识 AOS

抽象类 AQS 同样继承自抽象类 AOS(AbstractOwnableSynchronizer)

AOS 外部只有一个 Thread 类型的变量,提供了获取和设置以后独占锁线程的办法

次要作用是 记录以后占用独占锁(互斥锁)的线程实例

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    // 独占线程(不参加序列化)private transient Thread exclusiveOwnerThread;
    // 设置以后独占的线程
    protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}
    // 返回以后独占的线程
    protected final Thread getExclusiveOwnerThread() {return exclusiveOwnerThread;}
}

为什么要把握 AQS

如何可能体现程序员的程度,那就是把握大多数人所不把握的技术,这也是为什么面试时 AQS 高频呈现的起因,因为它不简略

最后接触 ReentrantLock 以及 AQS 的时候,看到源码就是一头雾水,Debug 跟着跟着就 迷失了本人,置信这也是大多数人的反馈

正是因为经验过,所以能力从小白的心理上登程,把其中的知识点可能尽数梳理

独占加锁源码解析

什么是独占锁

独占锁也叫排它锁,是指该锁一次只能被一个线程所持有,如果别的线程想要获取锁,只有等到持有锁线程开释

取得排它锁的线程即能读数据又能批改数据,与之对抗的就是共享锁

共享锁是指该锁可被多个线程所持有。如果线程 T 对数据 A 加上共享锁后,则其余线程只能对 A 再加共享锁,不能加排它锁

取得共享锁的线程只能读数据,不能批改数据

独占锁加锁

ReentrantLock 就是独占锁的一种实现形式,接下来看代码中如何应用 ReentrantLock 实现独占式加锁业务逻辑

public static void main(String[] args) {
    // 创立非偏心锁
    ReentrantLock lock = new ReentrantLock();
    // 获取锁操作
    lock.lock();
    try {// 执行代码逻辑} catch (Exception ex) {// ...} finally {
        // 解锁操作
        lock.unlock();}
}

new ReentrantLock() 构造函数默认创立的是非偏心锁 NonfairSync

public ReentrantLock() {sync = new NonfairSync();
}

同时也能够在创立锁构造函数中传入具体参数创立偏心锁 FairSync

ReentrantLock lock = new ReentrantLock(true);
--- ReentrantLock
// true 代表偏心锁,false 代表非偏心锁
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

FairSync、NonfairSync 代表偏心锁和非偏心锁,两者都是 ReentrantLock 动态外部类,只不过实现不同锁语义

偏心锁 FairSync

  1. 偏心锁是指多个线程依照申请锁的程序来获取锁,线程间接进入队列中排队,队列中的第一个线程能力取得锁
  2. 偏心锁的长处是期待锁的线程不会饿死。毛病是整体吞吐效率绝对非偏心锁要低,期待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非偏心锁大

非偏心锁 NonfairSync

  1. 非偏心锁是多个线程加锁时间接尝试获取锁,获取不到才会到期待队列的队尾期待。但如果此时锁刚好可用,那么这个线程能够无需阻塞间接获取到锁
  2. 非偏心锁的长处是能够缩小唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞间接取得锁,CPU 不用唤醒所有线程。毛病是处于期待队列中的线程可能会饿死,或者等很久才会取得锁

两者的都继承自 ReentrantLock 动态形象外部类 Sync,Sync 类继承自 AQS,这里就有个疑难

这些锁都没有间接继承 AQS,而是定义了一个 Sync 类去继承 AQS,为什么要这样呢?

因为 锁面向的是应用用户 同步器面向的则是线程管制 ,那么在锁的实现中聚合同步器而不是间接继承 AQS 就能够很好的  隔离二者所关注的事件

通过对不同锁品种的解说以及 ReentrantLock 内部结构的解析,依据上下级关系继承图,加深其了解

这里以非偏心锁举例,查看加锁的具体过程,详细信息下文会具体阐明

看一下非偏心锁加锁办法 lock 外部怎么做的

ReentrantLock lock = new ReentrantLock();
lock.lock();
--- ReentrantLock
public void lock() {sync.lock();
}
--- Sync
abstract void lock();

Sync#lock 为形象办法,最终会调用其子类非偏心锁的办法 lock

final void lock() {if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

非偏心加锁办法有两个逻辑

  1. 通过比拟并替换 State(同步状态)胜利与否决定是否取得锁,设置 State 为 1 示意胜利获取锁,并将以后线程设置为独占线程
  2. 批改 State 值失败则进入尝试获取锁流程,acquire 办法为 AQS 提供的办法

compareAndSetState 以 CAS 比拟并替换的形式将 State 值设置为 1,示意同步状态被占用

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

setExclusiveOwnerThread 设置以后线程为独占锁领有线程

protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}

acquire 对整个 AQS 做到了承前启后的作用,通过 tryAcquire 模版办法进行尝试获取锁,获取锁失败包装以后线程为 Node 节点退出期待队列排队

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

tryAcquire 是 AQS 中形象模版办法,然而外部会有默认实现,尽管默认的办法外部抛出异样,为什么不间接定义为形象办法呢?

因为 AQS 不只是对独占锁实现了形象,同时还包含共享锁;不同锁定义了不同类别的办法,共享锁就不须要 tryAcquire,如果定义为形象办法,继承 AQS 子类都须要实现该办法

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

NonfairSync 类中有 tryAcquire 重写办法,持续查看具体如何进行非偏心形式获取锁

protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();
    int c = getState();
   // State 等于 0 示意此时无锁
    if (c == 0) {
       // 再次应用 CAS 尝试获取锁, 体现为非偏心锁个性
        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
        setState(nextc);
        return true;
    }
   // 如果 state 不等于 0 并且独占线程不是以后线程, 返回 false
    return false;
}

因为 tryAcquire 做了取反,如果设置 state 失败并且独占锁线程不是本人自身返回 false,通过取反会进入接下来的流程

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

Node 入队流程

尝试取得锁失败,接下来会将线程组装成为 Node 进行入队流程

Node 是 AQS 中最根本的数据结构,也是 CLH 变体队列中的节点,Node 有 SHARED(共享)、EXCLUSIVE(独占) 两种模式,文章次要介绍 EXCLUSIVE 模式,不相干的属性和办法不予介绍

上面列出对于 Node EXCLUSIVE 模式的一些要害办法以及状态信息

Node 中独占锁相干的 waitStatus 属性别离有以下几种状态

介绍完 Node 相干基础知识,看一下申请锁线程如何被包装为 Node,又是如何初始化入队的

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);
    // 获取期待队列的尾节点
    Node pred = tail;
   // 如果尾节点不为空, 将 node 设置为尾节点, 并将原尾节点 next 指向 新的尾节点 node
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
   // 尾部为空,enq 执行
    enq(node);
    return node;
}

pred 为队列的尾节点,依据尾节点是否为空会执行对应流程

  1. 尾节点不为空,证实队列已被初始化,那么须要将对应的 node(以后线程)设置为新的尾节点,也就是入队操作;将 node 节点的前驱指针指向 pred(尾节点),并将 node 通过 CAS 形式设置为 AQS 期待队列的尾节点,替换胜利后将原来的尾节点后继指针指向新的尾节点
  2. 尾节点为空,证实还没有初始化队列,执行 enq 办法进行初始化队列

enq 办法执行初始化队列操作,期待队列中虚拟化的头节点也是在这里产生

private Node enq(final Node node) {for (; ;) {
        Node t = tail;
        if (t == null) {
           // 虚拟化一个空 Node, 并将 head 指向空 Node
            if (compareAndSetHead(new Node()))
               // 将尾节点等于头节点
                tail = head;
        } else {
           // node 上一条指向尾节点
            node.prev = t;
           // 设置 node 为尾节点
            if (compareAndSetTail(t, node)) {
               // 设置原尾节点的下一条指向 node
                t.next = node;
                return t;
            }
        }
    }
}

执行 enq 办法的前提就是队列尾节点为空,为什么还要再判断尾节点是否为空?

因为 enq 办法中是一个死循环,循环过程中 t 的值是不固定的。如果执行 enq 办法时队列为空,for 循环会执行两遍不同的解决逻辑

  1. 尾节点为空,虚拟化出一个新的 Node 头节点,这时队列中只有一个元素,为了保障 AQS 队列构造的完整性,会将尾节点指向头节点,第一遍循环完结
  2. 第二遍不满足尾节点为空条件,执行 else 语句块,node 节点前驱指针指向尾节点,并将 node 通过 CAS 设置为新的尾节点,胜利后设置原尾节点的后继指针指向 node,至此入队胜利。返回的 t 无意义,只是为了终止死循环

画两张图来了解 enq 办法整体初始化 AQS 队列流程,假如 T1、T2 两个线程争取锁,T1 胜利取得锁,T2 进行入队操作

  1. T2 进行入队操作,循环第一遍,尾节点为空。开始初始化头节点,并将尾节点指向头节点,最终队列模式是这样纸滴

  1. 循环第二遍,须要将 node 设置为新的尾节点。逻辑如下:尾节点不为空,设置 node 前驱指针指向尾节点,并将 node 设置为尾节点,原尾节点 next 指针指向 node

addWaiter 办法就是为了让 Node 入队,并且保护出一个双向队列模型

入队执行胜利后,会在 acquireQueued 再次尝试竞争锁,竞争失败后会将线程阻塞

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

acquireQueued 办法会尝试自旋获取锁,获取失败对以后线程施行阻塞流程,这也是为了防止无意义的自旋,比照 CLH 队列性能优化的体现

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (; ;) {
           // 获取 node 上一个节点
            final Node p = node.predecessor();
           // 如果 node 为头节点 & 尝试获取锁胜利
            if (p == head && tryAcquire(arg)) {
               // 此时以后 node 线程获取到了锁
               // 将 node 设置为新的头节点
                setHead(node);
               // help GC
                p.next = null;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {if (failed)
            cancelAcquire(node);
    }
}

通过 node.predecessor() 获取节点的前驱节点,前驱节点为空抛出空指针异样

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

获取到前驱节点后进行两步逻辑判断

  1. 判断前驱节点 p 是否为头节点,为 true 进行尝试获取锁,获取锁胜利设置以后节点为新的头节点,并将原头节点的后驱指针设为空
  2. 前驱节点不是头节点或者尝试加锁失败,执行线程休眠阻塞操作

如果 node 取得锁后,setHead 将节点设置为队列头,从而实现出队成果,出于 GC 的思考,清空未应用的数据

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

shouldParkAfterFailedAcquire 须要重点关注下,流程绝对比拟难了解

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
        pred.next = node;
    } else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

ws 示意为以后申请锁节点前驱节点的期待状态,代码中蕴含三个逻辑,别离是:

  1. ws == Node.SIGNAL,示意须要将申请锁节点进行阻塞
  2. ws > 0,示意期待队列中蕴含被勾销节点,须要调整队列
  3. 如果 ws == Node.SIGNAL || ws >0 都为 false,应用 CAS 的形式将前驱节点期待状态设置为 Node.SIGNAL

设置以后节点的前置节点期待状态为 Node.SIGNAL,示意以后节点获取锁失败,须要进行阻塞操作

还是通过几张图来了解流程,假如此时 T1、T2 线程来抢夺锁

T1 线程取得锁,T2 进入 AQS 期待队列排队,并通过 CAS 将 T2 节点的前驱节点期待状态置为 SIGNAL

执行切换前驱节点期待状态后返回 false,持续进行循环尝试获取同步状态

这一步操作保障了线程能进行多次重试,尽量避免线程状态切换

如果 T1 线程没有开释锁,T2 线程第二次执行到 shouldParkAfterFailedAcquire 办法,因为前驱节点已设置为 SIGNAL,所以会间接返回 true,执行线程阻塞操作

private final boolean parkAndCheckInterrupt() {
   // 将以后线程进行阻塞
    LockSupport.park(this);
   // 办法返回了以后线程的中断状态,并将以后线程的中断标识设置为 false
    return Thread.interrupted();}

LockSupport.park 办法将以后期待队列中线程进行阻塞操作,线程执行一个从 RUNNABLE 到 WAITING 状态转变

如果线程被唤醒,通过执行 Thread.interrupted 查看中断状态,这里的中断状态会被传递到 acquire 办法

public final void acquire(int arg) {if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       // 如果线程被中断, 这里会再次设置中断状态
       // 因为如果线程中断, 调用 Thread.interrupted 尽管会返回 true, 然而会革除线程中断状态
        selfInterrupt();}

即便线程从 park 办法中唤醒后发现自己被中断了,然而不影响接下来的获取锁操作,如果须要设置线程中断来影响流程,能够应用 lockInterruptibly 取得锁,抛出查看异样 InterruptedException

cancelAcquire

勾销排队办法是 AQS 中比拟难的知识点,不容易被了解

当线程因为自旋或者异样等状况获取锁失败,会调用此办法进行勾销正在获取锁的操作

private void cancelAcquire(Node node) {
    // 不存在的节点间接返回
    if (node == null)
        return;

    node.thread = null;

    /**
     * waitStatus > 0 代表节点为勾销状态
     * while 循环会将 node 节点的前驱指针指向一个非勾销状态的节点
     * pred 等于以后节点的前驱节点(非勾销状态)*/
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // 获取过滤后的前驱节点的后继节点
    Node predNext = pred.next;

    // 设置 node 期待状态为勾销状态
    node.waitStatus = Node.CANCELLED;

    // 步骤一,如果 node 是尾节点,应用 CAS 将 pred 设置为新的尾节点

    if (node == tail && compareAndSetTail(node, pred)) {
       // 设置 pred(新 tail)的后驱指针为空
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
       // 步骤二,node 的前驱节点 pred(非勾销状态)!= 头节点
        if (pred != head 
             /**
              * 1. pred 期待状态等于 SIGNAL
              * 2. ws <= 0 并且设置 pred 期待状态为 SIGNAL
              */
             && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) 
             // pred 中线程不为空
             && pred.thread != null) {
            Node next = node.next;
           /**
            * 1. 以后节点的后继节点不为空
            * 2. 后继节点期待状态 <=0(示意非勾销状态)*/
            if (next != null && next.waitStatus <= 0)
               // 设置 pred 的后继节点设置为以后节点的后继节点
                compareAndSetNext(pred, predNext, next);
        } else {
           // 步骤三,如果以后节点为头节点或者上述条件不满足, 执行唤醒以后节点的后继节点流程
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

逻辑略微简单一些,比拟重要是以下三个逻辑

  1. 步骤一以后节点为尾节点的话,设置 pred 节点为新的尾节点,胜利设置后再将 pred 后继节点设置为空(尾节点不会有后继节点)
  2. 步骤二须要满足以下四个条件才会将前驱节点(非勾销状态)的后继指针指向以后节点的后继指针 1)以后节点不等于尾节点 2)以后节点前驱节点不等于头节点 3)前驱节点的期待状态不为勾销状态 4)前驱节点的领有线程不为空
  3. 如果不满足步骤二的话,会执行步骤三相干逻辑,唤醒后继节点

步骤一:

假如以后勾销节点为尾节点并且前置节点无勾销节点,现有期待队列如下图,执行下述逻辑

if (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null);
}

将 pred 设置为新的尾节点,并将 pred 后继节点设置为空,因为尾节点不会有后继节点了

T4 线程所在节点因无援用指向,会被 GC 垃圾回收解决

步骤二:

如果以后须要勾销节点的前驱节点为勾销状态节点,如图所示

设置 pred(非勾销状态)的后继节点为 node 的后继节点,并设置 node 的 next 为 本人自身

线程 T2、T3 所在节点因为被 T4 所间接或间接指向,如何进行 GC?

AQS 期待队列中勾销状态节点会在 shouldParkAfterFailedAcquire 办法中被 GC 垃圾回收

if (ws > 0) {
    do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
    pred.next = node;
}

T4 线程所在节点获取锁失败尝试进行时,会执行上述代码,执行后的期待队列如下图所示

期待队列中勾销状态节点就能够被 GC 垃圾回收了,至此加锁流程也就完结了,上面持续看如何解锁

独占解锁源码解析

解锁流程绝对于加锁简略了很多,调用对应 API-lock.unlock()

--- ReentrantLock
public void unlock() {sync.release(1);
}
--- AQS
public final boolean release(int arg) {
   // 尝试开释锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

开释锁同步状态

tryRelease 是定义在 AQS 中的形象办法,通过 Sync 类重写了其实现

protected final boolean tryRelease(int releases) {int c = getState() - releases;
   // 如果以后线程不等于领有锁线程, 抛出异样
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
       // 将领有锁线程设置为空
        setExclusiveOwnerThread(null);
    }
   // 设置 State 状态为 0, 解锁胜利
    setState(c);
    return free;
}

唤醒后继节点

此时 State 值已被开释,对于头节点的判断这块流程比拟有意思

Node h = head;
if (h != null && h.waitStatus != 0)
  unparkSuccessor(h);

什么状况下头节点为空,当线程还在抢夺锁,队列还未初始化,头节点必然是为空的

当头节点期待状态等于 0,证实后继节点还在自旋,不须要进行后继节点唤醒

如果同时满足上述两个条件,会对期待队列头节点的后继节点进行唤醒操作

private void unparkSuccessor(Node node) {
   // 获取 node 期待状态
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
   // 获取 node 的后继节点
    Node s = node.next;
   // 如果下个节点为空或者被勾销, 遍历队列查问非勾销节点
    if (s == null || s.waitStatus > 0) {
        s = null;
       // 从队尾开始查找, 期待状态 <= 0 的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
   // 满足 s != null && s.waitStatus <= 0
   // 执行 unpark
    if (s != null)
        LockSupport.unpark(s.thread);
}

为什么查找队列中未被勾销的节点须要从尾部开始?

这个问题有两个起因能够解释,别离是 Node 入队和清理勾销状态的节点

  1. 先从 addWaiter 入队时说起,compareAndSetTail(pred, node)、pred.next = node 并非原子操作,如果在执行 pred.next = node 前进行 unparkSuccessor,就没有方法通过 next 指针向后遍历,所以才会从后向前找寻非勾销的节点
  2. cancelAcquire 办法也有导致应用 head 无奈遍历全副 Node 的因素,因为先断开的是 next 指针,prev 指针并未断开

唤醒阻塞后流程

当线程获取锁失败被 park 后进入了阻塞模式,前驱节点开释锁后会进行唤醒 unpark,被阻塞线程状态回归 RUNNABLE 状态

private final boolean parkAndCheckInterrupt() {
   // 从此地位唤醒
    LockSupport.park(this);
    return Thread.interrupted();}

被唤醒线程查看本身是否被中断,返回本身中断状态到 acquireQueued

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);
    }
}

假如本身被中断,设置 interrupted = true,持续通过循环尝试获取锁,获取锁胜利后返回 interrupted 中断状态

中断状态自身并不会对加锁流程产生影响,被唤醒后还是会一直进行获取锁,直到获取锁胜利进行返回,返回中断状态是为了后续补充中断纪录

如果线程被唤醒后发现中断,胜利获取锁后会将中断状态返回,补充中断状态

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

selfInterrupt 就是对线程中断状态的一个补充,补充状态胜利后,流程完结

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

浏览源码小技巧

1、从全局把握要浏览的源码提供了什么性能

这也是我始终推崇的学习源码形式,学习源码的关键点是抓住主线流程,在理解主线之前不要最开始就钻研到源码实现细节中,否则很容易迷失在细枝末节的代码中

以文章中的 AQS 举例,当你晓得了它是一个形象队列同步器,应用它能够更简略的结构锁和同步器等实现

而后从中了解 tryAcquire、tryRelease 等办法实现,这样是不是能够更好的了解与 AQS 与其子类相干的代码

2、把不易了解的源码粘贴进去,整顿好格局打好备注

个别源码中的行为格局和咱们日常敲代码是不一样的,而且 JDK 源码中的变量命名切实是惨不忍睹

所以就应该将难以了解的源码粘贴出,标上对应正文以及调整成易了解的格局,这样对于源码的浏览就会轻松很多

正文完
 0