【Java并发专题之三】Java线程互斥、协作原理
(I)Java线程互斥原理之synchronized原理
从JDK5引入CAS原子操作,但没有对synchronized关键字做优化,而是增加了J.U.C.concurrent,concurrent包有更好的性能;从JDK6对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。
优化后的锁有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。
一、无锁状态
线程的阻塞和唤醒需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,另外在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的,所以尽量通过不加锁来阻塞线程的方案。
1、自旋锁
指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
适用场景:
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。
缺点:
自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程不会尽快释放锁,自旋的线程就会白白消耗掉处理的资源,带来性能上的浪费。
使用:
在JDK 1.4.2中引入,默认关闭,可使用-XX:+UseSpinning开开启,在JDK1.6中默认开启;
自旋的次数必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起,默认自旋次数为10次,通过参数-XX:PreBlockSpin来调整
2、自适应自旋锁
自旋次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
机制:
线程如果自旋成功了,那么下次自旋的次数会加多,JVM认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,JVM对程序锁的状况预测会越来越准确,JVM会变得越来越聪明。
使用:JDK1.6引入。
3、锁消除
在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。
示例:
使用JDK的StringBuffer、Vector、HashTable等时,会存在隐形的加锁操作,比如StringBuffer的append()方法,Vector的add()方法,如果JVM根据数据流做逃逸分析之后,确定没有竞争,就会去掉加锁操作。
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
4、锁粗化
如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,那么将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
举例:上面vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
5、java对象头
对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充:
5.1 Mark Word结构
对象头里Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,存储对象自身的运行时数据:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
Mark Word被设计成一个非固定数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间。
举例32位机器对象头Mark Word结构:
5.1 Mark Word内容
随着锁级别的不同,对象头里会存储不同的内容:
(1)对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象本身的哈希码;
(2)偏向锁存储的是当前占用此对象的线程ID,判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较;
(3)而轻量级则存储指向线程栈中锁记录的指针,判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较;
二、偏向锁
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低。
设计目的:为了在没有多线程竞争的情况下尽量减少不必要的轻量锁执行,因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令操作,造成性能消耗。
使用:JDK6引进了偏向锁且默认开启,-XX:-UseBiasedLocking来禁止偏向锁
优点:在只有一个线程执行同步块时,避免CAS操作 进一步提高性能。
缺点:一旦出现多线程竞争的情况就必须撤销偏向锁,带来撤销操作的性能损耗
1、CAS为什么会引入本地延迟?
(1)SMP-对称多处理器架构:所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。
(2)CAS(Compare-And-Swap)是一条CPU原子指令,其作用是让CPU比较后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
举例说明:
Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”.
从这个层面来说,偏向锁设计的终极目标便是减少Cache一致性流量。而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。
2、偏向锁的执行
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可。
处理流程如下:
(1)检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
(2)若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
(3)如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
(4)通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
(5)执行同步代码块;
3、偏向锁的撤销
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点是上没有正在执行的代码)。
其步骤如下:
(1)暂停拥有偏向锁的线程;
(2)判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以允许其余线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头Mark Word,升级为轻量级锁状态(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式;
注意:此处将当前线程挂起再恢复的过程中并没有发生锁的转移,仍然在当前线程手中,只是穿插了个 “将对象头中的线程ID变更为指向锁记录地址的指针” 这么个事。
三、轻量级锁
设计目的:“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但要强调的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。
适用场景:线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁。
1、Lock Record
Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表。每一个被锁住的对象Mark Word和一个Lock Record关联,对象头的MarkWord中的Lock Word指向Lock Record的起始地址。
结构:
结构名称 | 描述 |
Owner | 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL |
EntryQ | 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程 |
RcThis | 表示blocked或waiting在该monitor record上的所有线程的个数 |
Nest | 用来实现 重入锁的计数 |
HashCode | 保存从对象头拷贝过来的HashCode值(可能还包含GC age) |
Candidate | 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁 |
2、轻量级锁的执行
(1)在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
此时线程堆栈与对象头的状态如下图所示:
(2)拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。
(3)拷贝成功后,JVM将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5);
为什么会尝试CAS不成功以及什么情况下会不成功?
CAS本身是不带锁机制的,其是通过比较而来。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操作前的对象HashCode了,所以CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。
然后线程B进行CAS自旋,等待对象头的锁标识重新变回无锁状态或对象头内容等于对象HashCode(因为这是线程B做CAS操作前的值),这也就意味着线程A执行结束(参见后面轻量级锁的撤销,只有线程A执行完毕撤销锁了才会重置对象头),此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。如果线程A的执行时间较长,线程B经过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待重新调度。
(4)如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,此时线程堆栈与对象头的状态如下图所示:
(5)如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
3、轻量级锁的撤销
轻量级锁的释放通过CAS操作来进行的,主要步骤如下:
(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
(2)如果替换成功,整个同步过程就完成了,恢复到无锁状态(01);
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;
对于轻量级锁,其性能提升的依据是 “对于绝大部分的锁,在整个生命周期内(交替执行)都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。
四、重量级锁
Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址(这是为什么Java中任意对象可以作为锁的原因)。synchronized内部锁或监视器锁(Monitor)来实现的,依赖底层操作系统的Mutex Lock来实现,操作系统实现线程之间切换需要从用户态转到内核态,成本高,状态转换需要相对长的时间,这就是为什么Synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。
1、监视器锁-Monitor
(1)Monitor结构
在JVM中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; //指向持有ObjectMonitor对象的线程 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
(2)同步过程
当多个线程同时访问一段同步代码时:
(2.1)首先会进入_EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
(2.2)若线程调用wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入_WaitSet集合中等待被唤醒;
(2.3)若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
(3)用法
当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this);
当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
2、同步原理举例
2.1 同步块
当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?
示例代码:
package test; public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
反汇编(javap -c SynchronizedDemo):
Compiled from "SynchronizedDemo.java" public class test.SynchronizedDemo { public test.SynchronizedDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void method(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3 // String Method 1 start 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any }
同步过程:
(1)monitorenter指令
插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
过程如下:
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
(2)monitorexit指令:
插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit,执行monitorexit的线程必须是objectref所对应的monitor的所有者。
过程如下:
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁;
2.2 同步方法
示例代码:
package test; public class SynchronizedDemo { public synchronized void method() { System.out.println("Hello World!"); } }
反汇编(javap -c -s -v -l SynchronizedMethod):
Classfile /E:/workspace/jdk1.8/out/production/jdk1.8/test/SynchronizedMethod.class Last modified 2019-12-17; size 516 bytes MD5 checksum a2cec0523e5c8d3f819fd36482d83002 Compiled from "SynchronizedMethod.java" public class test.SynchronizedMethod minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#17 // java/lang/Object."<init>":()V #2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #20 // Hello World! #4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #23 // test/SynchronizedMethod #6 = Class #24 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Ltest/SynchronizedMethod; #14 = Utf8 method #15 = Utf8 SourceFile #16 = Utf8 SynchronizedMethod.java #17 = NameAndType #7:#8 // "<init>":()V #18 = Class #25 // java/lang/System #19 = NameAndType #26:#27 // out:Ljava/io/PrintStream; #20 = Utf8 Hello World! #21 = Class #28 // java/io/PrintStream #22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V #23 = Utf8 test/SynchronizedMethod #24 = Utf8 java/lang/Object #25 = Utf8 java/lang/System #26 = Utf8 out #27 = Utf8 Ljava/io/PrintStream; #28 = Utf8 java/io/PrintStream #29 = Utf8 println #30 = Utf8 (Ljava/lang/String;)V { public test.SynchronizedMethod(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltest/SynchronizedMethod; public synchronized void method(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Ltest/SynchronizedMethod; } SourceFile: "SynchronizedMethod.java"
同步过程:
通过反汇编结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成,不过相对于普通方法,其方法区中多了ACC_SYNCHRONIZED标示符。
JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
总结:
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
五、锁的转换和对比
1、整体转换流程图
2、锁变化对应java对象头的变化
3、锁对比
各种锁并不是相互代替的,而是在不同场景下的不同选择,绝对不是说重量级锁就是不合适的:
如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;
如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;
如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;
(II)Java线程协作原理之wait¬ify原理
一、源码
Object中的5方法,其中有3个方法是native,由JVM本地c代码执行的;有2个wait重载wait(long)的方法:
public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait() throws InterruptedException { wait(0); } public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } // 此处对于纳秒的处理不精准,只是简单增加了1毫秒, if (nanos > 0) { timeout++; } wait(timeout); }
wait方法:wait是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁,所以wait必须要写在synchronized代码块中,否则会报异常。
notify方法:调用这两个方法也需要先获得该对象的锁。notify,notifyAll,唤醒等待该对象同步锁的线程,并放入该对象的锁池中。notify,notifyAll调用时并不会释放对象锁
如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来,并非随机唤醒(默认策略,退出同步块之后才唤醒,并非立即唤醒);
如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,然后这个线程退出同步块的时候继续唤醒其倒数第二个进入wait状态的线程,依次类推,即LIFO的策略(默认策略,可以调整);
1、执行过程分析
(1)一个线程通过1号门进入Entry Set(入口区): 如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码; 如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待; (2)线程在持有监视器的过程中(临界区),有两个选择: 一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器; 等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。 (3)等待区中的线程: 只能从3号门进,4号门出,即一个线程只有在持有监视器时才执行wait操作,处于等待线程只有再次获得监视器才退出等待状态。 (4)当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器: 如果入口区的线程赢了,会从2号门进入; 如果等待区的线程赢了会从4号门进入。
2、性能影响
wait/nofity是通过JVM里的park/unpark 机制来实现的,在Linux下这种机制又是通过pthread_cond_wait/pthread_cond_signal来玩的,当线程进入到wait状态的时候其实是会放弃cpu的,也就是说这类线程是不会占用cpu资源,不影响CPU load。
参考: