线程安全是并发编程中关注的重点,应该注意到的是,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,Java 引入了互斥锁的概念,对共享数据变量在访问前需要获取锁,然后才能对其进行修改,修改完后再释放锁,没有获取到锁的线程只能等待,直到当前线程处理完毕释放该锁。这样能够保证在同一时刻只有一个线程能够对共享数据进行操作,保证了多线程下的线程安全。在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized 另外一个重要的作用,synchronized 可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到。
synchronized 底层原理
Java 虚拟机中的同步是基于进入和退出管程(Monitor)对象实现,无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解 synchronized 实现原理非常关键。
Java 对象头与 Monitor
在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。这三块区域的作用分别是:
- 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
- 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头包括两部分:Mark Word 和 类型指针。
- Mark WordMark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。
- 类型指针类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。
Mark Word记录了对象和锁有关的信息,当这个对象被 synchronized 关键字当成同步锁时,围绕这个锁的一系列操作都和 Mark Word 有关。 Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。 下面附上 openjdk 关于对象头 markword 描述的源码说明,markword 的结构,定义在 markOop.hpp 文件:
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//[ptr | 00] locked ptr points to real header on stack
//[header | 0 | 01] unlocked regular object header
//[ptr | 10] monitor inflated lock (header is wapped out)
//[ptr | 11] marked used by markSweep to mark an object
64 位的 Mark Word 示意图
![e5f1097d38121ea50e5bb2a19229b30c.png](https://i-blog.csdnimg.cn/blog_migrate/08757f2e03bc4aeae56fddd4ca96dd42.jpeg)
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。轻量级锁是 00,重量级锁是 10。
JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
其中轻量级锁和偏向锁是 Java 6 对 synchronized 锁进行优化后新增加的,稍后会进行分析。这里我们主要分析一下重量级锁也就是通常说 synchronized 的对象锁,锁标识位为10,其中指针指向的是 monitor 对象的起始地址。当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的,其主要数据结构如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件,C++ 实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表(每个等待锁的线程都会被封装成 ObjectWaiter 对象),_owner 指向持有 ObjectMonitor 对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程同时 monitor 中的计数器 count 加 1,若线程调用 wait() 方法,将释放当前持有的monitor,owner 变量恢复为 null,count 自减 1,同时该线程进入 WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor(锁) 并复位变量的值,以便其他线程进入获取 monitor(锁)。
synchronized 代码块底层原理
首先看一下 synchronized 代码的简单例子
public class SynchronizedDemo {
public int i = 0;
public void add() {
synchronized (this) {
i++;
}
}
}
通过 javap 反编译后的结果如下,这里只贴重点部分
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // 进入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit // 退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 退出同步方法
22: aload_2
23: athrow
24: return
从字节码中可知同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置,当执行 monitorenter 指令时,当前线程将试图获取对象锁所对应的 monitor 的持有权,当对象锁的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有对象锁的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有对象锁的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit 指令被执行,执行线程将释放 monitor 并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个 monitorexit 指令,它就是异常结束时被执行的释放 monitor 的指令。
synchronized 方法的底层原理
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放 monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个 monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的 monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:
java 代码:
public class SynchronizedDemo {
public int i = 0;
public synchronized void add() {
i++;
}
}
通过 javap 反编译后的结果如下,这里只贴重点部分
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 8: 0
line 9: 10
synchronized 锁优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。
偏向锁
偏向锁是 Java 6 之后加入的新锁,它是一种针对加锁操作的优化手段。在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,比如 StringBuffer 的 append 方法。因此为了减少同一线程获取锁的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果。需要注意的是,如果有多线程争抢锁的情况,那么偏向锁就会失效,会升级为轻量级锁。
偏向锁的上锁过程偏向锁的获取方式是将对象头的 MarkWord 部分中, 标记上线程ID, 以表示哪一个线程获得了偏向锁。 具体的赋值逻辑如下: 首先读取目标对象的 MarkWord, 判断是否处于可偏向的状态
如果为可偏向状态, 则尝试用 CAS 操作, 将自己的线程 ID 写入MarkWord
如果 CAS 操作成功, 则认为已经获取到该对象的偏向锁, 执行同步块代码 。
补充: 一个线程在执行完同步代码块以后, 并不会尝试将 MarkWord 中的 thread ID 赋回原值 。这样做的好处是: 如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下, 直接认为偏向成功。
如果 CAS 操作失败, 则说明, 有另外一个线程 Thread B 抢先获取了偏向锁。 这种状态说明该对象的竞争比较激烈, 此时需要撤销 Thread B 获得的偏向锁,将 Thread B 持有的锁升级为轻量级锁。 该操作需要等待全局安全点 JVM safepoint ( 此时间点, 没有线程在执行字节码) 。
如果是已偏向状态, 则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。
- 如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块
- 如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁
从上面的偏向锁机制描述中,可以注意到: 偏向锁的 撤销(revoke) 是一个很特殊的操作, 为了执行撤销操作, 需要等待全局安全点(Safe Point), 此时间点所有的工作线程都停止了字节码的执行。
轻量级锁
偏向锁升级成为轻量级锁的条件是当有另外一个线程出现争抢锁的时候,会发生偏向锁的撤销操作。偏向锁撤销后, 对象可能处于两种状态。
- 一种是不可偏向的无锁状态,之所以不允许偏向, 是因为已经检测到了多于一个线程的竞争, 升级到了轻量级锁的机制。
- 一种是不可偏向的已锁 ( 轻量级锁) 状态
之所以会出现上述两种状态, 是因为偏向锁不存在解锁的操作, 只有撤销操作。 触发撤销操作时:
- 原来已经获取了偏向锁的线程可能已经执行完了同步代码块, 使得对象处于 “闲置状态”,相当于原有的偏向锁已经过期无效了。此时该对象就应该被直接转换为不可偏向的无锁状态。
- 原来已经获取了偏向锁的线程也可能尚未执行完同步代码块, 偏向锁依旧有效, 此时对象就应该被转换为被轻量级加锁的状态
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,因为操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
synchronized 锁升级过程详解
首先上图说明一下锁升级的流程
![0ae4faa5b6431c51de94cab1a301390b.png](https://i-blog.csdnimg.cn/blog_migrate/6ee31caadd866fbee106436530e43624.jpeg)
膨胀过程:无锁(锁对象初始化时)-> 偏向锁(有线程请求锁) -> 轻量级锁(多线程轻度竞争)-> 重量级锁(线程过多或长耗时操作,线程自旋过度消耗cpu)
jvm默认延时4s自动开启偏向锁(此时为匿名偏向锁,不指向任务线程),可通过 -XX:BiasedLockingStartUpDelay=0 取消延时;如果不要偏向锁,可通过-XX:-UseBiasedLocking = false来设置
锁只能升级,不能降级;偏向锁可以被重置为无锁状态
锁对象头记录占用锁的线程信息,但不能主动释放,线程栈同时记录锁的使用信息,当有其他线程(T1)申请已经被占用的锁时,先根据锁对向的信息,找对应线程栈,若线程已结束,则锁对象先被置为无锁状态,再被T1线程占有后置为偏向锁;若线程位结束,则锁状态由当前偏向锁升级为轻量级锁
偏向锁和轻量级锁在用户态维护,重量级锁需要切换到内核态(os)进行维护
下面使用 JOL 来验证锁升级的过程
无锁
public static void printNoLock() {
Object o = new Object();
System.out.println("hash: " + o.hashCode());
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
可以看到,header从高位到低位依次为:00000000 00000000 00000000 01010001 00011101 01010000 11000000 00000001,低位的最后三位是001,无锁状态。
![7833712d351f631dc43fdc6fa7c6414c.png](https://i-blog.csdnimg.cn/blog_migrate/1054222bf9271e5de8de81f4e0f89f68.png)
匿名偏向和偏向锁
public static void printPianXiangLock() throws Exception {
TimeUnit.SECONDS.sleep(5); // 等待 JVM 开启偏向锁
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
第一次打印为匿名偏向,第二次偏向锁指向了main线程
![f23227e57788a4bb12fcfbd5239ec63f.png](https://i-blog.csdnimg.cn/blog_migrate/a5d296d0543f9f58c28c92c75ebccab7.png)
轻量级锁
public static void printQingLiangLock() throws Exception {
TimeUnit.SECONDS.sleep(5);
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
for (int i = 0; i 1; i++) {
new Thread(() -> {
print(o);
}).start();
}
}
public static void print(Object o) {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
下图展示了由偏向锁升级到轻量级锁的过程,第一次打印的时候无多线程竞争,synchronized 锁为轻量级,之后由于又有新的线程争抢同一把锁,锁状态进行了升级,成为了轻量级锁。
![52bfde9c4b60c0e9532cfc6c1c77797e.png](https://i-blog.csdnimg.cn/blog_migrate/1454335171224d54040d0530bd0cb2db.png)
重量级锁
public static void printZhongLiangLock() throws Exception {
TimeUnit.SECONDS.sleep(5);
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
for (int i = 0; i 2; i++) {
new Thread(() -> {
print(o);
}).start();
}
}
public static void print(Object o) {
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
![44199e89cf8a8e2d7e5664c84efaaf0a.png](https://i-blog.csdnimg.cn/blog_migrate/b70b03acca8f5652edb09eb3d80cd6f7.png)
关于 synchronized 的其他关键点
synchronized 的可重入性
在 Java 中 synchronized 是基于原子性的内部锁机制,是可重入的,因此在一个线程调用 synchronized 方法的同时在其方法体内部调用该对象另一个 synchronized 方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是 synchronized 的可重入性,每次重入,monitor中的计数器会加 1。具体代码如下:
/**
* Synchronized 可重入性
*/
public class ReEnterSynchronized {
public static void main(String[] args) throws InterruptedException {
Accounting accounting = new Accounting();
Thread t1 = new Thread(accounting);
Thread t2 = new Thread(accounting);
t1.start();
t2.start();
t1.join();
t2.join();
}
public static class Accounting implements Runnable {
public static int n = 0;
public static int count = 0;
@Override
public void run() {
for (int i = 0; i 10000; i++) {
synchronized (this) {
n++;
increase();
}
}
}
private synchronized void increase() {
count++;
}
}
}
线程中断与 synchronized
线程中断
在 Java 中,提供了以下 3 个有关线程中断的方法
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),代码演示:
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread() {
@Override
public void run() {
try {
while (true) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("interrupted when sleep");
boolean interrupted = this.isInterrupted();
System.out.println("interrupt: " + interrupted);
}
}
};
t.start();
TimeUnit.SECONDS.sleep(3);
t.interrupt();
}
}
输出结果:
interrupted when sleep
interrupt: false
除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用 Thread.interrupt()
中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:
/**
* 非阻塞情况下的线程不会被中断, 线程会一直执行下去
*/
public class NoInterruptThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("未被中断");
}
}
};
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
}
这种情况下我们要手动进行中断检测并终止程序,修改后的代码如下:
/**
* 非阻塞的线程,需要手动进行中断检测,判断线程是否被中断
*/
public class NoBlockThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread() {
@Override
public void run() {
while (true) {
if (interrupted()) {
System.out.println("线程中断");
break;
} else {
System.out.println("未被中断");
}
}
System.out.println("线程结束运行");
}
};
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
}
输出结果:
未被中断
未被中断
未被中断
线程中断
线程结束运行
线程中断与 synchronized
线程的中断操作对于正在等待获取的锁对象的 synchronized 方法或者代码块并不起作用,也就是对于synchronized 来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。代码演示如下:
/**
* 线程中断与 Synchronized,线程在获取 Synchronized 对象锁的时候,是不会被中断的
*/
public class InterruptSynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
InterruptSynchronized sync = new InterruptSynchronized();
Thread thread = new Thread(sync);
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
}
class InterruptSynchronized implements Runnable {
public synchronized void fun() {
System.out.println("call func");
while (true) {
// never release lock
}
}
public InterruptSynchronized() {
new Thread() {
@Override
public void run() {
fun();
}
}.start();
}
@Override
public void run() {
while (true) {
if (Thread.interrupted()) {
System.out.println("线程中断");
break;
} else {
fun();
}
}
}
}
等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是 notify/notifyAll 和 wait 方法,在使用这 3 个方法时,必须处于 synchronized 代码块或者 synchronized 方法中,否则就会抛出 IllegalMonitorStateException 异常,这是因为调用这几个方法前必须拿到当前对象的监视器 monitor 对象,也就是说 notify/notifyAll 和 wait 方法依赖于 monitor 对象,在前面的分析中,我们知道 monitor 存在于对象头的 Mark Word 中(存储 monitor 引用指针),而 synchronized 关键字可以获取 monitor ,这也就是为什么 notify/notifyAll 和 wait 方法必须在 synchronized 代码块或者 synchronized 方法调用的原因。
需要特别理解的一点是,与 sleep 方法不同的是 wait 方法调用完成后,线程将被暂停,但 wait 方法将会释放当前持有的监视器锁(monitor),直到有线程调用 notify/notifyAll 方法后方能继续执行,而 sleep 方法只让线程休眠并不释放锁。同时 notify/notifyAll 方法调用后,并不会马上释放监视器锁,而是在相应的 synchronized(){}/synchronized 方法执行结束后才自动释放锁。