上一篇文章中咱们一块儿学习了jvm缓存一致性、多线程间的原子性、有序性、指令重排的相关内容,
这一篇文章便开始和你们一块儿学习学习AQS(AbstractQueuedSynchronizer)的内容
主要是包含如下三个方面java
synchronized ReentrantLock AbstractQueuedSynchronizer
1.了解并发同步器node
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的状况;这种资源多是:对象、变量、文件等。 共享:资源能够由多个线程同时访问 可变:资源能够在其生命周期内被修改 由此能够得出 因为线程执行的过程是不可控的,因此须要采用同步机制来协同对对象可变状态的访问
那么咱们怎么解决线程并发安全问题?spring
实际上,全部的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。 即在同一时刻,只能有一个线程访问临界资源,也称做同步互斥访问。 Java 中,提供了两种方式来实现同步互斥访问:synchronized和Lock
同步器的本质就是加锁shell
加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问) 不过有一点须要区别的是: 当多个线程执行一个方法时,该方法内部的局部变量 并非临界资源, 由于这些局部变量是在每一个线程的私有栈中,所以不具备共享性,不会致使线程安全问题
其中锁包括 显式锁 和 隐式锁编程
显式: ReentrantLock数组
ReentrantLock,实现juc里Lock,实现是基于AQS实现,须要手动加锁跟解锁ReentrantLock lock(),unlock();
隐式: Synchronized缓存
Synchronized加锁机制,Jvm内置锁,不须要手动加锁与解锁,Jvm会自动加锁跟解锁
synchronized原理详解安全
synchronized内置锁是一种对象锁(锁的是对象而非引用),做用粒度是对象,能够用来实现对临界资源的同步互斥访问,是可重入的 如下是他的三种加锁方式: 加锁的方式: 同步实例方法,锁是当前实例对象(加入spring容器管理的,锁是当前实例对象的时候,不能是多例的) 同步类方法,锁是当前类对象 同步代码块,锁是括号里面的对象 JVM内置锁经过synchronized使用,经过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步, 监视器锁的实现依赖底层操做系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低 /** *越过jvm直接操做内存的工具 * @author njw */ public class UnsafeInstance { public static Unsafe reflectGetUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { e.printStackTrace(); } return null; } /** * 不使用lock,怎么实现跨方法进行加锁和释放? * 方法:能够经过Unsafe来实现 * synchronized底层实现字节码翻译以后 即是如此的 */ private Object object = new Object(); public void test(){ reflectGetUnsafe().monitorEnter(object); } public void test1(){ reflectGetUnsafe().monitorExit(object); } }
synchronized 底层原理数据结构
synchronized是基于JVM内置锁实现,经过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步, 监视器锁的实现依赖底层操做系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。 JVM内置锁在1.5以后版本作了重大的优化,如锁粗化(LockCoarsening)、锁消除(Lock Elimination)、 轻量级锁(LightweightLocking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减小锁操做的开销,内置锁的并发性能已经基本与Lock持平。 synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置
每一个同步对象都有一个本身的Monitor(监视器锁),加锁过程以下图所示:
那么有个问题来了,咱们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?多线程
答案是锁状态是被记录在每一个对象的对象头(Mark Word)中,下面咱们一块儿认识一下对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局能够分为三块区域:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。 对象头:好比 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等 实例数据:即建立对象时,对象中成员变量,方法等 对齐填充:对象的大小必须是8字节的整数倍
对象头
HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等, 这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits, 官方称它为“Mark Word”。对象须要存储的运行时数据不少,其实已经超出了3二、64位Bitmap结构所能记录的限度, 可是对象头信息是与对象自身定义的数据无关的额 外存储成本,考虑到虚拟机的空间效率, Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,它会根据对象的状态复用本身的存储空间。 例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode), 4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0, 对象头信息是与对象自身定义的数据无关的额外存储成本,可是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽可能多的数据, 它会根据对象的状态复用本身的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态以下
可是若是对象是数组类型,则须要三个机器码,由于JVM虚拟机能够经过Java对象的元数据信息肯定Java对象的大小, 可是没法从数组的元数据来确认数组的大小,因此用一块来记录数组长度.
在此提出一个问题:程序中,实例对象内存 存储在哪?
不少人了解到的都是实例对象存储在 堆内存 中,确实,基本上实例对象内存都是存在堆内存中的
若是实例对象存储在堆区时:实例对象内存存在堆区,实例的引用存在栈上,实例的元数据class存在方法区或者元空间
但实际上Object实例对象是不必定是存在堆区的,若是实例对象发生了 线程逃逸行为 则其内存将可能存在 线程栈中
下面就这个问题来分析一下
使用逃逸分析的状况,编译器能够对代码作以下优化
1、同步省略。 若是一个对象被发现只能从一个线程被访问到,那么对于这个对象的操做能够不考虑同步。 2、将堆分配转化为栈分配。 若是一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象多是栈分配的候选,而不是堆分配。 3、分离对象或标量替换。 有的对象可能不须要做为一个连续的内存结构存在也能够被访问到,那么对象的部分(或所有)能够不存储在内存, 而是存储在CPU寄存器中。
在Java代码运行时,经过JVM参数可指定是否开启逃逸分析,
XX:+DoEscapeAnalysis : 表示开启逃逸分析 XX:DoEscapeAnalysis :表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,须要指定XX:DoEscapeAnalysis
逃逸分析代码
/** * 线程逃逸 分析 * @author njw */ public class StackAllocTest { /** * 进行两种测试 * 1. 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,若是有GC信息将会被打印出来 * VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError * * 以管理员方式运行 power shell * jps 查看进程 :6080 StackAllocTest * jmap -histo 6080 * 结果 * 1: 740 70928456 [I * 2: 500000 12000000 com.it.edu.sample.StackAllocTest$TestStudent * 50W个对象 * * * * 2. 开启逃逸分析 * VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError * * 结果 * 1: 740 79444704 [I * 2: 145142 3483408 com.it.edu.sample.StackAllocTest$TestStudent * 只有145142个 * */ public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 500000; i++) { alloc(); } long end = System.currentTimeMillis(); //查看执行时间 System.out.println("cost-time " + (end - start) + " ms"); try { Thread.sleep(100000); } catch (InterruptedException e1) { e1.printStackTrace(); } } /** * 在主线程中不停建立TestStudent 按照正常逻辑思考 循环50W次,建立后堆区 里面就会有50W的对象 * 若是堆区里面远远小于50W个 可能对象就存在当前线程栈中 * 考虑到是否发生GC,当前把GC回收日记打印出来,并同时调大堆空间,避免堆内GC的发生 * * 存在栈中的缘由: * Jit对编译时会对代码进行 逃逸分析() * 并非全部对象存放在堆区,有的一部分存在线程栈空间 * @return */ private static TestStudent alloc() { TestStudent student = new TestStudent(); return student; } static class TestStudent { private String name; private int age; } }
3.局面内置锁的升级
JDK1.6版本以后对synchronized的实现进行了各类优化,如自旋锁、偏向锁和轻量级锁,并默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁,可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
下图为锁的升级全过程
jvm锁的升级详解
32位jvm对象存储图
JVM锁的膨胀升级_无锁到重量级锁
偏向锁
偏向锁是Java 6以后加入的新锁,它是一种针对加锁操做的优化手段,通过研究发现,在大多数状况下,锁不只不存在多线程竞争, 并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。 偏向锁的核心思想是, 若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需 再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。 因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。 可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁, 不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
假若偏向锁失败,虚拟机并不会当即升级为重量级锁, 适用于线程交替执行,同步代码逻辑少 所需执行执行时间比较少的 它还会尝试使用一种称为轻量级锁的优化手段(1.6以后加入的),此时Mark Word 的结构也变为轻量级锁的结构。 轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。 须要了解的是,轻量级锁所适应的场景是 **线程交替执行同步块** 的场合,若是存在同一时间访问同一锁的场合, 就会致使轻量级锁膨胀为重量级锁
自旋锁
轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。 这是基于在大多数状况下,线程持有锁的时间都不会太长, 若是直接挂起操做系统层面的线程可能会得不偿失,毕竟操做系统实现线程之间的切换时须要从用户态转换到核心态, 这个状态之间的转换须要相对比较长的时间,时间成本相对较高,所以自旋锁会假设在不久未来,当前的线程能够得到锁, 所以虚拟机会让当前想要获取锁的线程作几个空循环(这也是称为自旋的缘由),通常不会过久,多是50个循环或100循环, 在通过若干次循环后,若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做系统层面挂起, 这就是自旋锁的优化方式,这种方式确实也是能够提高效率的。最后没办法也就只能升级为重量级锁了
锁消除 和 锁的粗化
消除锁是虚拟机另一种锁的优化,这种优化更完全, Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描, 去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间, 例如说 StringBuffer的append是一个同步方法,可是在add方法中的StringBuffer属于一个局部变量, 而且不会被其余线程所使用,所以StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除
代码分析,锁的粗化和消除
/** * * JVM对锁的优化 * 1.锁的粗化 * 2.锁的消除 * * @author njw */ public class Test { StringBuffer stb = new StringBuffer(); /** * 锁的粗化 * * StringBuffer 调用 append的时候,锁加在当前对象上 * 按照正常逻辑思考 下面调用了 四次 append,至关于加了四个同步块 * synchronized{ * stb.append("1"); * } * synchronized{ * stb.append("2"); * } * ... * * 若是是这样意味着此次操做要进行四次上下文切换,四次加锁,四次释放锁 * * 可是jvm通过优化,会把 四个变成一个,加成了一个统一的全局锁 这就是锁的粗化 * synchronized{ * stb.append("1"); * stb.append("2"); * } * */ public void test1(){ //jvm的优化,锁的粗化 stb.append("1"); stb.append("2"); stb.append("3"); stb.append("4"); } /** * 锁的消除 * * synchronized (new Object()) { * //伪代码:不少逻辑 * } * jvm是否会对上面代码进行加锁? * 答案 这里jvm不会对这同步块进行加锁 * * 这里的代码中 jvm会进行逃逸分析 * 由于:new Object()这个加锁对象中,这个new Object()并不会被其余线程访问到,加锁并无意义,不会产生线程 逃逸 * 因此这里不会加锁 这即是 JVM 锁的消除 * * 具体状况查看 逃逸分析 优化 * */ public void test2(){ //jvm的优化,JVM不会对同步块进行加锁 synchronized (new Object()) { //伪代码:不少逻辑 //jvm是否会加锁? //jvm会进行逃逸分析 } } public static void main(String[] args) { Test test = new Test(); } }
Java并发编程核心在于java.concurrent.util包
而juc当中的大多数同步器实现都是围绕着共同的基础行为,好比 等待队列、条件队列、独占获取(排他锁)、共享获取(共享) 等
而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS
AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器
state 会记录加锁状态、次数等 ,使框架有了可重复入的特性 独占获取 抽象除了排他锁 共享获取 抽象除了共享锁 等待队列,条件队列 使其具有了公平、非公平特性
AQS具有特性
阻塞等待队列 共享/独占 公平/非公平 可重入 容许中断
例如Java.concurrent.util当中同步器的实现如Lock,Latch,Barrier等,都是基于AQS框架实现
通常经过定义内部类Sync继承AQS 将同步器全部调用都映射到Sync对应的方法
AQS内部维护属性volatile int state (32位)
state表示资源的可用状态
State三种访问方式
getState()、setState()、compareAndSetState()
AQS定义两种资源共享方式
Exclusive-独占,只有一个线程能执行,如ReentrantLock
Share-共享,多个线程能够同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
同步等待队列
条件等待队列
不一样的自定义同步器争用共享资源的方式也不一样。
自定义同步器在实现时只须要实现共享资源state的获取与释放方式便可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),
AQS已经在顶层实现好了。
自定义同步器实现时主要实现如下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才须要去实现它。 tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int):共享方式。尝试释放资源,若是释放后容许唤醒后续等待结点返回true,不然返回false。
CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,
是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改成阻塞机制
Condition是一个多线程间协调通讯的工具类,使得某个,或者某些线程一块儿等待某个条件(Condition),
只有当该条件具有时 ,这些等待线程才会被唤醒,从而从新争夺锁
写锁(独享锁、排他锁),是指该锁一次只能被一个线程所持有。若是线程T对数据A加上排它锁后,则其余线程不能再对A加任何类型的锁。得到写锁的线程即能读数据又能修改数据。 读锁(共享锁)是指该锁可被多个线程所持有。若是线程T对数据A加上共享锁后,则其余线程只能对A再加共享锁,不能加排它锁。得到读锁的线程只能读数据,不能修改数据。 AQS中state字段(int类型,32位),此处state上分别描述读锁和写锁的数量因而将state变量“按位切割”切分红了两个部分 高16位表示读锁状态(读锁个数) 低16位表示写锁状态(写锁个数)
1.Node节点介绍
static final class Node { /** * 标记节点未共享模式 * */ static final Node SHARED = new Node(); /** * 标记节点为独占模式 */ static final Node EXCLUSIVE = null; /** * 在同步队列中等待的线程等待超时或者被中断,须要从同步队列中取消等待 * 在队列节点构建的时候 假如一个节点加入等待队列 会在加入的时候检查其余队列中旳节点是否处于 这个状态,若是是的话就剔除, * 而且继续检查其余的? * */ static final int CANCELLED = 1; /** * 后继节点的线程处于等待状态,而当前的节点若是释放了同步状态或者被取消, * 将会通知后继节点,使后继节点的线程得以运行。 * * 此状态是能够被唤醒的 能够去获取锁 */ static final int SIGNAL = -1; /** * 处于等待队列 * 该状态说明 节点在等待队列中,节点的线程等待在Condition上,当其余线程对Condition调用了signal()方法后, * 该节点会从 等待队列 中转移到 同步队列 中,加入到同步状态的获取中 */ static final int CONDITION = -2; /** * 表示下一次共享式同步状态获取将会被 无条件地传播下去 * 假如线程t1 执行完以后,广播发现t2,处于 PROPAGATE 状态,能够无条件去唤醒,并继续检查t3 */ static final int PROPAGATE = -3; /** * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态 * 使用CAS更改状态,volatile保证线程可见性,高并发场景下, * 即被一个线程修改后,状态会立马让其余线程可见。 */ volatile int waitStatus; /** * 前驱节点,当前节点加入到同步队列中被设置 */ volatile Node prev; /** * 后继节点 */ volatile Node next; /** * 节点同步状态的线程 */ volatile Thread thread; /** * TODO 这个节点用在条件队列中 信号灯 * * 等待队列中的后继节点,若是当前节点是共享的,那么这个字段是一个SHARED常量, * 也就是说节点类型 (独占和共享)和 等待队列中 的后继节点共用同一个字段。 */ Node nextWaiter; /** * 判断是否共享 * Returns true if node is waiting in shared mode. */ 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; } }
2.FairSync 公平锁
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; @Override final void lock() { acquire(1); } /** * 重写aqs中的方法逻辑 * 尝试加锁,被AQS的acquire()方法调用 */ @Override protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 表示当前没有任何线程加锁,能够去加锁 if (c == 0) { /** * 与非公平锁中的区别,须要先判断队列当中是否有等待的节点 * 若是没有则能够尝试CAS获取锁 : 使用原子操做更新 状态 * compareAndSetState : 依赖于 unsafe 操做执行原子比较操做 */ // hasQueuedPredecessors: 判断是否头结点不等于尾结点 同时 头结点的下一个为空,或者头结点的下一个不是当前线程 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; } }
3.NonfairSync 非公平锁
NonfairSync 定义 static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * 加锁行为 */ @Override final void lock() { /** * 第一步:直接尝试加锁 * 与公平锁实现的加锁行为一个最大的区别在于,此处不会去判断同步队列(CLH队列)中是否有排队等待加锁的节点, * 一上来就直接加锁(判断state是否为0,CAS修改state为1) * 并将独占锁持有者 exclusiveOwnerThread 属性指向当前线程 * 若是当前有人占用锁,再尝试去加一次锁 */ if (compareAndSetState(0, 1)) { // 尝试修改拥有线程为当前线程 setExclusiveOwnerThread(Thread.currentThread()); } else { //AQS定义的方法,加锁 acquire(1); } } /** * 父类AbstractQueuedSynchronizer.acquire()中调用本方法 */ @Override protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } NonfairSync 获取锁 /** * 尝试获取非公平锁 */ final boolean nonfairTryAcquire(int acquires) { //acquires = 1 final Thread current = Thread.currentThread(); int c = getState(); /** * 不须要判断同步队列(CLH)中是否有排队等待线程 * 判断state状态是否为0,为0能够加锁 */ if (c == 0) { //unsafe操做,cas修改state状态 if (compareAndSetState(0, acquires)) { //独占状态锁持有者指向当前线程 setExclusiveOwnerThread(current); return true; } } /** * state状态不为0,判断锁持有者是不是当前线程, * 若是是当前线程持有 则state+1 */ 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定义的方法,加锁 public final void acquire(int arg) { // tryAcquire 实际调用的子类方法 if (!tryAcquire(arg) && // addWaiter 首先添加一个节点在队列中 添加到尾部 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } addWaiter 添加节点: private Node addWaiter(Node mode) { // 1. 将当前线程构建成Node类型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 2. 判断 当前尾节点是否为null? if (pred != null) { // 2.2 将当前节点尾插入的方式,插入到尾部 // 将新创的结点的prev(前驱节点)指向本来的tail节点 node.prev = pred; // 2.3 使用CAS将节点插入同步队列的尾部 if (compareAndSetTail(pred, node)) { // 若是插入成功 把本来的tail的下一个节点指向 当前新建的结点 而后返回当前节点 pred.next = node; return node; } } // 把节点加入CLH同步队列 主要是 单前tail 是空的话 上面的逻辑没执行到,里面有个相似的结点指向操做 enq(node); return node; } /** * 节点加入CLH同步队列 */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize //队列为空须要初始化,建立空的头节点 if (compareAndSetHead(new Node())) tail = head; } else { // 队列中已经有值 尾节点不是空 把当前传进来的结点的 prev节点指向 当前tail节点 node.prev = t; //set尾部节点 if (compareAndSetTail(t, node)) {//当前节点置为尾部 t.next = node; //前驱节点的next指针指向当前节点 return t; } } } } acquireQueued: /** * 已经在队列当中的Thread节点,准备阻塞等待获取锁 */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //死循环自旋 for (;;) { //找到当前结点的前驱结点 final Node p = node.predecessor(); // 若是前驱结点是头结点,才tryAcquire,其余结点是没有机会tryAcquire的。 if (p == head && tryAcquire(arg)) { //获取同步状态成功,将当前结点设置为头结点。 setHead(node); p.next = null; // help GC failed = false; return interrupted; } /** * 若是前驱节点不是Head,经过shouldParkAfterFailedAcquire判断是否应该阻塞 * 前驱节点信号量为-1,当前线程能够安全被parkAndCheckInterrupt用来阻塞线程 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) { interrupted = true; } } } finally { if (failed) { cancelAcquire(node); } } } parkAndCheckInterrupt: /** * 阻塞当前节点,返回当前Thread的中断状态 * LockSupport.park 底层实现逻辑调用系统内核功能 pthread_mutex_lock 阻塞线程 */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//阻塞 return Thread.interrupted(); } shouldParkAfterFailedAcquire: private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //判断是否应该阻塞 // 获取前驱节点等待状态 int ws = pred.waitStatus; // 此状态是能够被唤醒的 能够去获取锁 if (ws == Node.SIGNAL) /* * 若前驱结点的状态是SIGNAL,意味着当前结点能够被安全地park */ return true; if (ws > 0) { /* 状态是 1 被移除,而且继续检查其余节点,若是都是取消状态 一并移除 * 前驱节点状态若是被取消状态,将被移除出队列 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* 同步队列不会出现 CONDITION * 因此 当前驱节点waitStatus为 0 or PROPAGATE(可传递状态)状态时 * * 将其设置为SIGNAL状态,而后当前结点才能够能够被安全地park */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }