【java虚拟机】之并发编程-锁和同步相关知识-LOCK
一、java虚拟机的内存模型以及操作语义
1.1、整体结构
1.2、操作语义
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
1.2.1、规则约定
- 【read和load顺序性保障】Java内存模型只要求上述两个操作必须按顺序执行,但不要求是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a
- 【read和load,stroe和write必须成对出现】不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现。
- 【assign出现则必须将值刷回主内存】不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 【无assign则不允许副本刷回主内存】不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
- 【一个新变量必须在主内存诞生】一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use、store操作之前,必须先执行assign和load操作。
- 【lock变量只可以一个线程获得,且可重入】一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
- 【lock后以主内存的值为准】如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化变量的值。
- 【unlock只能是lock成对出现】如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
- 【unlock前需要将工作内存变更刷回主内存】对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
二、java虚拟机的锁synchronized,volatile,Lock
2.1、偏向锁,轻量级锁,重量锁的优缺点
2.2、锁膨胀的原理和说明
锁膨胀:【无锁】=>【偏向锁】=>【轻量级锁】=>【重量级锁】
【偏向锁】
此时当 Thread#1 进入临界区时,JVM 会将 lockObject 的 对象头 Mark Word 的锁标志位设为“01”,同时会用 CAS 操作把 Thread#1 的线程 ID 记录到 Mark Word 中,此时进 入偏向模式。所谓“偏向”,指的是这个锁会偏向于 Thread#1, 若接下来没有其他线程进入临界区,则 Thread#1 再出入 临界区无需再执行任何同步操作。也就是说,若只有 Thread#1 会进入临界区,实际上只有 Thread#1 初次进入 临界区时需要执行 CAS 操作,以后再出入临界区都不会有 同步操作带来的开销。
【轻量级锁】
偏向锁的场景太过于理想化,更多的时候是 Thread#2 也 会尝试进入临界区, 如果 Thread#2 也进入临界区但是 Thread#1 还没有执行完同步代码块时,会暂停 Thread#1 并且升级到轻量级锁。Thread#2 通过自旋再次尝试以轻量 级锁的方式来获取锁
【重量级锁】
如果 Thread#1 和 Thread#2 正常交替执行,那么轻量级锁 基本能够满足锁的需求。但是如果 Thread#1 和 Thread#2 同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意 味着 Thread#1 线程获得了重量级锁的情况下,Thread#2 就会被阻塞
2.3、java并发编程的锁-Lock接口
写的比较好的锁的文章:https://baijiahao.baidu.com/s?id=1706248894950321913&wfr=spider&for=pc
2.3.1、锁接口-Lock的示意
/** * 获取锁: * 如果当前锁资源空闲可用则获取锁资源返回, * 如果不可用则阻塞等待,不断竞争锁资源,直至获取到锁返回。 */ void lock(); /** * 释放锁: * 当前线程执行完成业务后将锁资源的状态由占用改为可用并通知阻塞线程。 */ void unlock(); /** * 获取锁:(与lock方法不同的在于可响应中断操作,即在获取锁过程中可中断) * 如果当前锁资源可用则获取锁返回。 * 如果当前锁资源不可用则阻塞直至出现如下两种情况: * 1.当前线程获取到锁资源。 * 2.接收到中断命令,当前线程中断获取锁操作。 */ void lockInterruptibly() throws InterruptedException; /** * 非阻塞式获取锁: * 尝试非阻塞式获取锁,调用该方法获取锁立即返回获取结果。 * 如果获取到了锁则返回true,反之返回flase。 */ boolean tryLock(); /** * 非阻塞式获取锁: * 根据传入的时间获取锁,如果线程在该时间段内未获取到锁返回flase。 * 如果当前线程在该时间段内获取到了锁并未被中断则返回true。 */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * 获取等待通知组件(该组件与当前锁资源绑定): * 当前线程只有获取到了锁资源之后才能调用该组件的wait()方法, * 当前线程调用await()方法后,当前线程将会释放锁。 */ Condition newCondition();
2.3.2、锁接口-ReentrantLock的示意
// 查询当前线程调用lock()的次数 int getHoldCount() // 返回目前持有此锁的线程,如果此锁不被任何线程持有,返回null protected Thread getOwner(); // 返回一个集合,它包含可能正等待获取此锁的线程,其内部维持一个队列(后续分析) protected Collection<Thread> getQueuedThreads(); // 返回正等待获取此锁资源的线程估计数 int getQueueLength(); // 返回一个集合,它包含可能正在等待与此锁相关的Condition条件的线程(估计值) protected Collection<Thread> getWaitingThreads(Condition condition); // 返回调用当前锁资源Condition对象await方法后未执行signal()方法的线程估计数 int getWaitQueueLength(Condition condition); // 查询指定的线程是否正在等待获取当前锁资源 boolean hasQueuedThread(Thread thread); // 查询是否有线程正在等待获取当前锁资源 boolean hasQueuedThreads(); // 查询是否有线程正在等待与此锁相关的Condition条件 boolean hasWaiters(Condition condition); // 返回当前锁类型,如果是公平锁返回true,反之则返回flase boolean isFair() // 查询当前线程是持有当前锁资源 boolean isHeldByCurrentThread() // 查询当前锁资源是否被线程持有 boolean isLocked()
2.3.3、同步队列管理器:AQS的原理
在之前的《彻底理解Java并发编程之Synchronized关键字实现原理剖析》中谈到过,synchronized重量级锁底层的实现是基于ObjectMonitor对象中的计数器实现的,而在AQS中也存在着异曲同工之处,它内部通过一个用volatile关键字修饰的int类型全局变量state作为标识来控制同步状态。当状态标识state为0时,代表着当前没有线程占用锁资源,反之当状态标识state不为0时,代表着锁资源已经被线程持有,其他想要获取锁资源的线程必须进入同步队列等待当前持有锁的线程释放。AQS通过内部类Node构建FIFO(先进先出)的同步队列用来处理未获取到锁资源的线程,将等待获取锁资源的线程加入到同步队列中进行排队等待。同时AQS使用内部类ConditionObject用来构建等待队列,当Condition调用await()方法后,等待获取锁资源的线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁资源的竞争。值得我们注意的是在这里存在两种类型的队列:
①同步队列:当线程获取锁资源发现已经被其他线程占有而加入的队列;
②等待队列(可能存在多个):当Condition调用await()方法后加入的队列;
AQS同步队列模型如下:
publicabstractclass AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{ // 指向同步队列的头部 private transient volatile Node head; // 指向同步队列的尾部 private transient volatile Node tail; // 同步状态标识 private volatileint state; // 省略...... }
其中head以及tail是AQS的全局变量,其中head指向同步队列的头部,但是需要注意的是head节点为空不存储信息,而tail指向同步队列的尾部。AQS中同步队列采用这种方式构建双向链表结构方便队列进行节点增删操作。state则为我们前面所提到的同步状态标识,当线程在执行过程中调用获取锁的lock()方法后,如果state=0,则说明当前锁资源未被其他线程获取,当前线程将state值设置为1,表示获取锁成功。如果state=1,则说明当前锁资源已被其他线程获取,那么当前线程则会被封装成Node节点加入同步队列进行等待。Node节点是对每一个获取锁资源线程的封装体,其中包括了当前执行的线程本身以及线程的状态,如是否被阻塞、是否处于等待唤醒、是否中断等。每个Node节点中都关联着前驱节点prev以及后继节点next,这样能够方便持有锁的线程释放后能快速释放下一个正在等待的线程。
Node类结构如下:
staticfinalclass Node { // 共享模式 staticfinal Node SHARED = new Node(); // 独占模式 staticfinal Node EXCLUSIVE = null; // 标识线程已处于结束状态 staticfinalint CANCELLED = 1; // 等待被唤醒状态 staticfinalint SIGNAL = -1; // Condition条件状态 staticfinalint CONDITION = -2; // 在共享模式中使用表示获得的同步状态会被传播 staticfinalint PROPAGATE = -3; // 等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE四种 volatileint waitStatus; // 同步队列中前驱结点 volatile Node prev; // 同步队列中后继结点 volatile Node next; // 获取锁资源的线程 volatile Thread thread; // 等待队列中的后继结点(与Condition有关,稍后会分析) Node nextWaiter; // 判断是否为共享模式 final boolean isShared() { return nextWaiter == SHARED; } // 获取前驱结点 final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) thrownew NullPointerException(); else return p; } // 省略代码..... }
在其中SHARED和EXCLUSIVE两个全局常量分别代表着共享模式和独占模式,
【共享模式】:即允许多个线程同时对一个锁资源进行操作,例如:信号量Semaphore、读锁ReadLock等采用的就是基于AQS的共享模式实现的。
【独占模式】:则代表着在同一时刻只运行一个线程对锁资源进行操作,如ReentranLock等组件的实现都是基于AQS的独占模式实现。
全局变量waitStatus则代表着当前被封装成Node节点的线程的状态,一共存在五种情况:
-
0 初始值状态:waitStatus=0,代表节点初始化。
-
CANCELLED 取消状态:waitStatus=1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消该Node的节点,其节点的waitStatus为CANCELLED,进入该状态后的节点代表着进入了结束状态,当前节点将不会再发生变化。
-
SIGNAL 信号状态:waitStatus=-1,被标识为该状态的节点,当其前驱节点的线程释放了锁资源或被取消,将会通知该节点的线程执行。简单来说被标记为当前状态的节点处于等待唤醒状态,只要前驱节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程执行。
-
CONDITION 条件状态:waitStatus=-2,与Condition相关,被表示为该状态的节点处于等待队列中,节点的线程等待在Condition条件,当其他线程调用了Condition的signal()方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取竞争锁资源。
-
PROPAGATE 传播状态:waitStatus=-3,该状态与共享模式有关,在共享模式中,被标识为该状态的节点的线程处于可运行状态。
全局变量pre和next分别代表着当前Node节点对应的前驱节点和后继节点,thread代表当前被封装的线程对象。
nextWaiter代表着等待队列中,当前节点的后继节点(与Condition有关稍后分析)。
到这里其实我们对于Node数据类型的结构有了大概的了解了。
总之,AQS作为JUC的核心组件,对于锁存在两种不同的实现,即独占模式(如ReetrantLock)与共享模式(如Semaphore)。
但是不管是独占模式还是共享模式的实现类,都是建立在AQS的基础上实现,其内部都维持着一个队列,当试图获取锁的线程数量超过当前模式限制时则会将线程封装成一个Node节点加入队列进行等待。而这一系列操作都是由AQS帮我们完成,无论是ReetrantLock还是Semaphore,其实它们的绝大部分方法最终都是直接或间接的调用AQS完成的。
-
AbstractOwnableSynchronizer抽象类: 内部定义了存储当前持有锁资源线程以及获取存储线程信息方法。
-
AbstractQueuedSynchronizer抽象类: AQS指的就是AbstractQueuedSynchronizer的首字母缩写,整个AQS框架的核心类。内部以虚拟队列的形式实现了线程对于锁资源获取(tryAcquire)与释放(tryRelease),但是在AQS中没有对锁获取与锁释放的操作进行默认实现,具体的逻辑需要子类实现,这样使得我们在开发过程中能够更加灵活的运用它。
-
Node内部类: AbstractQueuedSynchronizer中的内部类,用于构建AQS内部的虚拟队列,方便于AQS管理需要获取锁的线程。
-
Sync内部抽象类: ReentrantLock的内部类,继承AbstractQueuedSynchronizer类并实现了其定义的锁资源获取(tryAcquire)与释放(tryRelease)方法,同时也定义了lock()方法,提供给子类实现。
-
NonfairSync内部类: ReentrantLock的内部类,继承Sync类,非公平锁的实现者。
-
FairSync内部类: ReentrantLock的内部类,继承Sync类,公平锁的实现者。
-
Lock接口: Java锁类的顶级接口,定义了一系列锁操作的方法,如:lock()、unlock()、tryLock等。
-
ReentrantLock: Lock锁接口的实现者,内部存在Sync、NonfairSync、FairSync三个内部类,在创建时可以根据其内部fair参数决定使用公平锁/非公平锁,其内部操作绝大部分都是基于间接调用AQS方法完成。
我们可以通过上面类图关系看出AQS是一个抽象类,但是在其源码实现中并不存在任何抽象方法,这是因为AQS设计的初衷更倾向于作为一个基础组件,并不希望直接作为操作类对外输出,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等。从设计模式角度来看,AQS采用的模板模式的模式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作及解锁操作,为什么这么做呢?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的具体逻辑实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过加锁/解锁的逻辑不同,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可。
2.3.4、实际开发过程中ReetrantLock与synchronized如何抉择?
在前面的文章:《彻底理解Java并发编程之Synchronized关键字实现原理剖析》中我们曾详细的谈到过Java中的隐式锁的synchronized的底层实现,我们也曾谈到在JDK1.6之后,JVM对于Synchronized关键字进行了很大程度上的优化,那么在实际开发过程中我们又该如何在ReetrantLock与synchronized进行选择呢?
- synchronized相对来说使用更加方便、语义更清晰,同时JVM也为我们自动优化了。
- ReetrantLock则使用起来更加灵活,同时也提供了多样化的支持,比如超时获取锁、可中断式获取锁、等待唤醒机制的多个条件变量(Condition)等。所以在我们需要用到这些功能时我们可以选择ReetrantLock。
但是具体采用哪个还是需要根据业务需求决定。
2.3.5、ReetrantLock实现总结
-
基础组件:
-
同步状态标识:对外显示锁资源的占有状态
-
同步队列:存放获取锁失败的线程
-
等待队列:用于实现多条件唤醒
-
Node节点:队列的每个节点,线程封装体
-
基础动作:
-
cas修改同步状态标识
-
获取锁失败加入同步队列阻塞
-
释放锁时唤醒同步队列第一个节点线程
-
加锁动作:
-
调用tryAcquire()修改标识state,成功返回true执行,失败加入队列等待
-
加入队列后判断节点是否为signal状态,是就直接阻塞挂起当前线程
-
如果不是则判断是否为cancel状态,是则往前遍历删除队列中所有cancel状态节点
-
如果节点为0或者propagate状态则将其修改为signal状态
-
阻塞被唤醒后如果为head则获取锁,成功返回true,失败则继续阻塞
-
解锁动作:
-
调用tryRelease()释放锁修改标识state,成功则返回true,失败返回false
-
释放锁成功后唤醒同步队列后继阻塞的线程节点
-
被唤醒的节点会自动替换当前节点成为head节点
2.3.1、Lock接口相关知识
锁基于CAS操作和同步队列实现。底层依赖CAS原子操作 和 LockSupport工具类。
获取锁的线程,位于同步队列器的【头节点】。
同步队列中Node的节点状态示意
-
0 初始值状态:waitStatus=0,代表节点初始化。
-
CANCELLED 取消状态:waitStatus=1,在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消该Node的节点,其节点的waitStatus为CANCELLED,进入该状态后的节点代表着进入了结束状态,当前节点将不会再发生变化。
-
SIGNAL 信号状态:waitStatus=-1,被标识为该状态的节点,当其前驱节点的线程释放了锁资源或被取消,将会通知该节点的线程执行。简单来说被标记为当前状态的节点处于等待唤醒状态,只要前驱节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程执行。
-
CONDITION 条件状态:waitStatus=-2,与Condition相关,被表示为该状态的节点处于等待队列中,节点的线程等待在Condition条件,当其他线程调用了Condition的signal()方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取竞争锁资源。
-
PROPAGATE 传播状态:waitStatus=-3,该状态与共享模式有关,在共享模式中,被标识为该状态的节点的线程处于可运行状态。
2.3.1、同步队列器的模板方法示意java.util.concurrent.locks.AbstractQueuedSynchronizer
【获取同步状态的模板方法流程图-该模板直到获取锁或线程被中断才会返回】
【尝试指定时间内获取同步状态的模板方法流程图-该模板获取锁 或者 等待超时时间已到,或者 已经超时,则返回】
2.3.1.1、获取同步状态的模板方法: acquire(int arg)
其中:tryAcquire(arg) 是可重写的,用于业务自定义获取同步状态的逻辑
public final void acquire(int arg) { //tryAcquire(arg) 表示尝试获取同步状态,如果获取成功,则返回true, 获取失败则返回false //addWaiter(Node.EXCLUSIVE):如果尝试你获取失败,则将当前线程形成一个node节点,加入到FIFO队列中 //acquireQueued(node, arg) :添加到队列中后,则尝试在队列中获取同步状态。(会阻塞线程),该方法返回true则表示当前线程被中断,返回false则表示正常获取到了锁 //selfInterrupt():当前线程如果被中断,则再次中断自己,将线程的中断标识设置为true if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
2.3.1.2、释放同步状态的模板方法: release(int arg)
其中: boolean tryRelease(int arg)是可重写的,用于业务自定义释放同步状态的逻辑
public final boolean release(int arg) { //tryRelease(arg) 尝试释放同步状态,如果释放成功则返回true,否则返回false // unparkSuccessor(h) 基于头节点,当头节点不为空,则等待状态不等于0时,会尝试唤醒头节点向后的第一个等待状态<0 的节点对应的线程,然后释放锁成功 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
2.3.1.3、同步器模板方法中不允许重写的方法释义
addWaiter(Node mode) //加入同步队列的方法
enq(final Node node) //死循环+CAS 加入同步队列中
//向同步器的队尾加入节点 private Node addWaiter(Node mode) { //形成一个新的节点,表示当前线程的节点 Node node = new Node(Thread.currentThread(), mode); //将队尾节点设置给方法变量 Node pred = tail; if (pred != null) { //如果队尾节点不为null,则表示队列中有排队的线程节点 //待加入节点的上一个节点指向尾巴节点 node.prev = pred; //基于CAS操作尝试将待加入节点,设置成队列中的尾巴节点 if (compareAndSetTail(pred, node)) { //CAS设置成功,则将上上一个节点的下一个节点指针指向加入节点 pred.next = node; return node; } } //情况1:如果队列中尾巴节点为空 //情况2:CAS操作设置尾巴节点失败 //则基于死循环将当前节点加入尾巴节点 enq(node); return node; } // 基于死循环+ CAS 操作,将待加入节点加入到队列中 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { //表示队列中为空的情况,则新生一个node,基于CAS操作设置为头节点 if (compareAndSetHead(new Node())) //如果设置成功,则头尾节点,执行相同的node tail = head; } else { //基于CAS操作将当前待加入节点,加入到队列的尾巴节点,直到加入成功,才退出死循环 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
//在队列中获取同步器(锁)的方法 final boolean acquireQueued(final Node node, int arg) { //是否获取锁成功的标识 boolean failed = true; try { //线程是否被中断的标识 boolean interrupted = false; for (;;) { //获取node的上一个节点 final Node p = node.predecessor(); //如果node的上一个节点是头节点则表示可以尝试获取锁 //tryAcquire(arg) 表示尝试获取锁 if (p == head && tryAcquire(arg)) { //获取锁成功,将自己设置成头节点 setHead(node); p.next = null; // 旧的头节点的下一个节点设置为null, 从双向链表队列中移除掉 //表示获取锁成功 false表示成功,true表示失败 failed = false; //返回线程是否被中断的标识 return interrupted; } //shouldParkAfterFailedAcquire 将队列中因线程中断 或 超时的节点,从队列中移除。(链表的断开和连接) // parkAndCheckInterrupt() 将当前线程阻塞,在阻塞返回时检查线程是否被中断 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //如果当前线程获取锁失败,则将当前线程形成的node节点的状态设置为取消状态 if (failed) cancelAcquire(node); } } // 获取node指向的上一个节点 final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } //检查node的上一个节点的信息 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //上一个节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果上一个节点的状态是 -1 ,表示等待获取锁 return true; if (ws > 0) { //上一个节点的状态大于0,标识节点对应的线程被中断或取消,则将上一个节点从链表中移除,将当前节点和未被取消的节点完成链表的链接。 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果上一个节点的状态 不等于-1,且不大于0,则积极与CAS将上一个节点的状态设置为 -1 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //阻塞当前线程,并检查线程的同步状态 private final boolean parkAndCheckInterrupt() { //阻塞当前线程 LockSupport.park(this); //当线程从阻塞中被唤醒,需要检查线程的中断状态 //Thread.interrupted() //情况1:该线程被中断,则线程中断标识为false,返回true //情况2:该线程未被中断,则线程中断标识为false,返回false return Thread.interrupted(); } //从同步队列中取消节点 private void cancelAcquire(Node node) { //节点为null返回 if (node == null) return; node.thread = null; Node pred = node.prev; //从node开始,向队列前端找,找到节点状态是等待中的节点。 while (pred.waitStatus > 0) node.prev = pred = pred.prev; //等待中状态的节点的下一个节点的对象 Node predNext = pred.next; //如果node是尾巴节点,则尝试CAS将找到的上一个节点,设置为尾巴节点 if (node == tail && compareAndSetTail(node, pred)) { //CAS尾巴节点的下一个节点指向null compareAndSetNext(pred, predNext, null); } else { // 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 } }
2.3.2、公平锁获取锁和释放锁的逻辑
2.3.2.1、公平锁获取锁的逻辑tryAcquire(arg)
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { //获取当前线程对象 final Thread current = Thread.currentThread(); //获取当前线程的状态 int c = getState(); //c=0,则表示已经没有线程持有同步状态 if (c == 0) { //hasQueuedPredecessors() 表示队列中是否有排队获取同步状态的节点 如果有则表示没有获取锁成功,需要执行模板方法加入队列中; 如果没有,则尝试CAS操作变更线程同步状态,变更成功则表示获取锁成功,否则表示获取锁失败,走后续流程加入队列 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; } }
2.3.2.2、公平锁释放锁的逻辑 tryRelease(int arg)
protected final boolean tryRelease(int releases) { //修改同步器的状态值 int c = getState() - releases; //异常情况检测:当前线程未持有同步器,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); //是否释放同步器成功的标志变量 boolean free = false; if (c == 0) { //等于0则表示释放成功,将同步器持有的线程对象设置为null free = true; setExclusiveOwnerThread(null); } //设置同步器状态数据 setState(c); return free; }
2.3.3、非公平锁获取锁和释放锁的逻辑
2.3.3.1、非公平锁获取锁的逻辑tryAcquire(arg)
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { //进入后,直接尝试变更同步器的状态,无需关注队列中是否有等待中的节点。 区别于公平锁的顺序获取。如果变更成功,则获取锁成功。 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else //如果未变更成功,则走获取同步状态的模板方法 acquire(1); } protected final boolean tryAcquire(int acquires) { //非公平锁获取同步状态的方法 return nonfairTryAcquire(acquires); } } 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; }
2.3.3.2、非公平锁释放锁的逻辑 tryRelease(int arg)
protected final boolean tryRelease(int releases) { //修改同步器的状态值 int c = getState() - releases; //异常情况检测:当前线程未持有同步器,则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); //是否释放同步器成功的标志变量 boolean free = false; if (c == 0) { //等于0则表示释放成功,将同步器持有的线程对象设置为null free = true; setExclusiveOwnerThread(null); } //设置同步器状态数据 setState(c); return free; }
2.3.4、读写锁
读写锁的状态设计
当前同步状态值为S
【写状态】=> 写状态等于S&0x0000FFFF(将高16位全部抹去),当写状态增加1时,等于S+1
【读状态】=> S>>>16(无符号补0右移16位),当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。
2.4、java并发编程的锁-等待条件
https://baijiahao.baidu.com/s?id=1706248894950321913&wfr=spider&for=pc
在Java并发编程中,每个Java堆中的对象在“出生”的时刻都会“伴生”一个监视器对象,而每个Java对象都会有一组监视器方法:wait()
、notify()
以及notifyAll()
。我们可以通过这些方法实现Java多线程之间的协作和通信,也就是等待唤醒机制,如常见的生产者-消费者模型。但是关于Java对象的这组监视器方法我们在使用过程中,是需要配合synchronized关键字才能使用,因为实际上Java对象的等待唤醒机制是基于monitor监视器对象实现的。在monitor监视器模型上,一个对象拥有一个同步队列和一个等待队列
Condition则更为灵活,因为synchronized的notify()
只能随机唤醒等待锁的一个线程,而Condition则可以更加细粒度的精准唤醒等待锁的某个线程。与synchronized的等待唤醒机制不同的是,AQS中一个锁对象拥有一个同步队列和多个等待队列。
==========synchronized实现等待和唤醒机制==================
==========Lock实现等待和唤醒机制==================
在前面我们提到过,Condition只是一个接口,具体的落地实现者为AQS内部的ConditionObject类,在本文最开始分析AQS时我们也曾提到,在AQS内部存在两种队列:同步队列以及等待队列,等待队列则是基于Condition而言的。同步队列与等待队列中的节点类型都是AQS内部的Node构成的,只不过等待队列中的Node节点的waitStatus为CONDITION状态。在ConditionObject类中存在两个节点:firstWaiter、lastWaiter用于存储等待队列中的队首节点以及队尾节点,每个节点使用Node.nextWaiter存储下一个节点的引用,因此等待队列是一个单向队列。所以AQS同步器的总体结构如下:
Condition接口的定义
publicinterfaceCondition { /** * 调用当前方法会使当前线程处于等待状态直到被通知(signal)或中断 * 当其他线程调用singal()或singalAll()方法时,当前线程将被唤醒 * 当其他线程调用interrupt()方法中断当前线程等待状态 * await()相当于synchronized等待唤醒机制中的wait()方法 */ void await() throws InterruptedException; /** * 作用与await()相同,但是该方法不响应线程中断操作 */ void awaitUninterruptibly(); /** * 作用与await()相同,但是该方法支持超时中断(单位:纳秒) * 当线程等待时间超出nanosTimeout时则中断等待状态 */ long awaitNanos(long nanosTimeout) throws InterruptedException; /** * 作用与awaitNanos(long nanosTimeout)相同,但是该方法可以声明时间单位 */ boolean await(long time, TimeUnit unit) throws InterruptedException; /** * 作用与await()相同,在deadline时间内被唤醒返回true,其他情况则返回false */ boolean awaitUntil(Date deadline) throws InterruptedException; /** * 当有线程调用该方法时,唤醒等待队列中的一个线程节点 * 并将该线程从等待队列移动同步队列阻塞等待锁资源获取 * signal()相当于synchronized等待唤醒机制中的notify()方法 */ void signal(); /** * 作用与signal()相同,不过该方法的作用是唤醒该等待队列中的所有线程节点 * signalAll()相当于synchronized等待唤醒机制中的notifyAll()方法 */ void signalAll(); }
与同步队列不同的是:每个Condition都对应一个等待队列,如果在一个ReetrantLock锁上创建多个Condition,也就相当于会存在多个等待队列。
//一个案例说明Condition的使用方式
public static class Queue { private Lock lock = new ReentrantLock(); //表示队列已经不是空队列的条件。 //=>当消费者发现队列为空会阻塞 //=>生产者放入数据后则需要通知消费者开始消费 private Condition notEmpty = lock.newCondition(); //表示队列已经不是满队列的条件。 //=>当生产者向队列中放入数据,发现队列已经满,则阻塞。 //=>当消费者消费掉一个数据后,则认为队列已经不是满,需要通知生产者继续放入数据。 private Condition notFull = lock.newCondition(); private Object[] queue = new Object[10]; private int count; //队列是否是满的 private boolean isFull() { return true; } //队列是否为空的 private boolean isEmpty() { return false; } //入队操作 private void inQueue(Object o) { } ; //出队操作 private Object outQueue() { return null; } //生产者 public void putData(Object data) throws InterruptedException { lock.lock(); try { //经过判断队列已经满了 while (isFull()) { notFull.await(); } //入队 inQueue(data); //唤醒队列开始消费 notEmpty.signal(); } finally { lock.unlock(); } } //消费者 public Object pollData() throws InterruptedException { lock.lock(); try { //判断队列是否为空 while (isEmpty()) { notEmpty.await(); } //取出数据 Object o = outQueue(); //唤醒生产者 notFull.signal(); return o; } finally { lock.unlock(); } } }
2.4.1、condition的实现原理
如上图,与同步队列不同的是:每个Condition都对应一个等待队列,如果在一个ReetrantLock锁上创建多个Condition,也就相当于会存在多个等待队列。同时,虽然同步队列与等待队列中的节点都是由Node类构成的,但是同步队列中的Node节点是存在pred前驱节点以及next后继节点引用的双向链表类型,而等待队列中的每个节点则只使用nextWaiter存储后继节点引用的单向链表类型。但是与同步队列一致,等待队列也是一种FIFO的队列,队列每个节点都会存储Condition对象上等待的线程信息。当一个线程调用await挂起类的方法时,该线程首先会释放锁,同时构建一个Node节点封装线程的相关信息,并将其加入等待队列,直到被唤醒、中断或者超时才会从队列中移除。下面我们从源码角度探究Condition等待/唤醒机制的原理:public final void await() throws InterruptedException { // 判断线程是否出现中断信号 if (Thread.interrupted()) // 响应中断则直接抛出异常中断线程执行 thrownew InterruptedException(); // 封装线程信息构建新的节点加入等待队列并返回 Node node = addConditionWaiter(); // 释放当前线程持有的锁锁资源,不管当前线程重入多少次,全部置0 int savedState = fullyRelease(node); int interruptMode = 0; // 判断节点是否在同步队列(SyncQueue)中,即是否被唤醒 while (!isOnSyncQueue(node)) { // 如果不需要唤醒,则在JVM级别挂起当前线程 LockSupport.park(this); // 判断是否被中断唤醒,如果是退出循环 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 被唤醒后执行自旋操作尝试获取锁,同时判断线程是否被中断 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; // 取消后进行清理 if (node.nextWaiter != null) // 清理等待队列中不为CONDITION状态的节点 unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } // 构建节点封装线程信息入队方法 private Node addConditionWaiter() { Node t = lastWaiter; // 判断节点状态是否为结束状态,如果是则移除 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } // 构建新的节点封装当前线程相关信息,节点状态为CONDITION等待状态 Node node = new Node(Thread.currentThread(), Node.CONDITION); // 将节点加入队列 if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
从如上代码观察中,不难发现,await()主要做了四件事:
-
一、调用addConditionWaiter()方法构建新的节点封装线程信息并将其加入等待队列
-
二、调用fullyRelease(node)释放锁资源(不管此时持有锁的线程重入多少次都一律将state置0),同时唤醒同步队列中后继节点的线程。
-
三、调用isOnSyncQueue(node)判断节点是否存在同步队列中,在这里是一个自旋操作,如果同步队列中不存在当前节点则直接在JVM级别挂起当前线程
-
四、当前节点线程被唤醒后,即节点从等待队列转入同步队列时,则调用acquireQueued(node, savedState)方法执行自旋操作尝试重新获取锁资源
至此,整个await()方法结束,整个线程从调用await()方法→构建节点入列→释放锁资源唤醒同步队列后继节点→JVM级别挂起线程→唤醒竞争锁资源流程完结。其他await()等待类方法原理类似则不再赘述,下面我们再来看看singal()唤醒方法:
public final void signal() { // 判断当前线程是否持有独占锁资源,如果未持有则直接抛出异常 if (!isHeldExclusively()) thrownew IllegalMonitorStateException(); Node first = firstWaiter; // 唤醒等待队列第一个节点的线程 if (first != null) doSignal(first); }
在这里,singal()唤醒方法一共做了两件事:
-
一、判断当前线程是否持有独占锁资源,如果调用唤醒方法的线程未持有锁资源则直接抛出异常(共享模式下没有等待队列,所以无法使用Condition)
-
二、唤醒等待队列中的第一个节点的线程,即调用doSignal(first)方法
private void doSignal(Node first) { do { // 移除等待队列中的第一个节点,如果nextWaiter为空 // 则代表着等待队列中不存在其他节点,那么将尾节点也置空 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 如果被通知上个唤醒的节点没有进入同步队列(可能出现被中断的情况), // 等待队列中还存在其他节点则继续循环唤醒后继节点的线程 } while (!transferForSignal(first) && (first = firstWaiter) != null); } // transferForSignal()方法 final boolean transferForSignal(Node node) { /* * 尝试修改被唤醒节点的waitStatus为0即初始化状态 * 如果设置失败则代表着当前节点的状态不为CONDITION等待状态, * 而是结束状态了则返回false返回doSignal()继续唤醒后继节点 * 为什么说设置失败则代表着节点不为CONDITION等待状态? * 因为可以执行到此处的线程必定是持有独占锁资源的, * 而此处使用的是cas机制修改waitStatus,失败的原因只有一种: * 预期值waitStatus不等于CONDITION */ if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) returnfalse; // 快速追加到同步队列尾部,同时返回前驱节点p Node p = enq(node); // 判断前驱节点状态是否为结束状态或者在设置前驱节点状态为SIGNAL失败时, // 唤醒被通知节点内的线程 int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) // 唤醒node节点内的线程 LockSupport.unpark(node.thread); returntrue; }
在如上代码中,可以通过我的注释发现,doSignal()也只做了三件事:
-
一、将被唤醒的第一个节点从等待队列中移除,然后再维护等待队列中firstWaiter和lastWaiter的指向节点引用
-
二、将等待队列中移除的节点追加到同步队列尾部,如果同步队列追加失败或者等待队列中还存在其他节点的话,则继续循环唤醒其他节点的线程
-
三、加入同步队列成功后,如果前驱节点状态已经为结束状态或者在设置前驱节点状态为SIGNAL失败时,直接通过LockSupport.unpark()唤醒节点内的线程
至此,Signal()方法逻辑结束,不过需要注意的是:我们在理解Condition的等待/唤醒原理的时候需要将await()/signal()方法结合起来理解。在signal()逻辑完成后,被唤醒的线程则会从前面的await()方法的自旋中退出,因为当前线程所在的节点已经被移入同步队列,所以while (!isOnSyncQueue(node))
条件不成立,循环自然则终止,进而被唤醒的线程会调用acquireQueued()
开始尝试获取锁资源。
三、操作系统和java如何实现原子操作
3.1、缓存锁定的机制
https://blog.csdn.net/qq_35642036/article/details/82801708
EMSI协议:https://zh.wikipedia.org/wiki/MESI%E5%8D%8F%E8%AE%AE
CPU缓存:https://coolshell.cn/articles/20793.html
缓存一致性:缓存一致性机制就整体来说,是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取,用MESI阐述原理如下:
MESI协议:是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:
M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
I:无效的。本CPU中的这份缓存已经无效。
一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。
当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。
当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下性能开销是相对较大的。在写入完成后,修改其缓存状态为M。
所以如果一个变量在某段时间只被一个线程频繁地修改,则使用其内部缓存就完全可以办到,不涉及到总线事务,如果缓存一会被这个CPU独占、一会被那个CPU 独占,这时才会不断产生RFO指令影响到并发性能。这里说的缓存频繁被独占并不是指线程越多越容易触发,而是这里的CPU协调机制,这有点类似于有时多线程并不一定提高效率,原因是线程挂起、调度的开销比执行任务的开销还要大,这里的多CPU也是一样,如果在CPU间调度不合理,也会形成RFO指令的开销比任务开销还要大。当然,这不是编程者需要考虑的事,操作系统会有相应的内存地址的相关判断