悲观锁
悲观锁(Pessimistic Locking)是一种悲观的并发控制机制,它基于悲观的假设,即并发冲突会时常发生,因此在访问共享资源(如数据库记录或共享变量)之前,会先获取独占性的锁,以防止其他线程对资源的并发读写。
悲观锁适用于写操作频繁、读操作较少的场景,能够确保数据一致性,但会引入较大的性能开销和线程切换的开销。
实现方式
在 Java 中,可以使用以下方式实现悲观锁:
synchronized
关键字:使用synchronized
关键字可以实现对共享资源的悲观锁。通过在方法或代码块中加上synchronized
关键字,只允许一个线程进入同步区域,并对共享资源进行操作。其他线程需要等待当前线程释放锁才能进入同步区域。
synchronized (sharedObject) {
// 进入同步区域,操作共享资源
}
ReentrantLock
类:ReentrantLock
是Java.util.concurrent
包提供的可重入锁实现。相较于 Synchronized,ReentrantLock 提供了更精细的锁控制,包括手动获取锁、手动释放锁、可重入性等特性。
注意事项
-
悲观锁的使用需要考虑锁的粒度,过大的锁粒度可能会影响并发性能,过小的锁粒度可能会导致频繁的锁竞争。
-
使用悲观锁时,应确保获取锁和释放锁的操作是成对出现的,否则可能会导致死锁或资源泄漏等问题。
-
需要谨慎处理异常情况,确保在异常发生时能够正确释放锁,避免其他线程被阻塞。
synchronized
synchronized
是Java中的关键字,用于实现线程的同步和互斥。synchronized
关键字可以用于修饰方法或代码块,用于控制对共享资源的访问。它的主要作用是 保证多个线程在并发执行时的安全性和一致性。
应用场景
-
修饰方法:可以使用
synchronized
修饰方法,将整个方法设置为同步方法。当一个线程进入同步方法时,会自动获取该方法所属对象(或类)的锁,在同步方法中:
- 对于非静态方法,使用的锁是当前实例对象(
this
)
- 对于非静态方法,使用的锁是当前实例对象(
public synchronized void method(){}
- 对于静态方法,锁为当前类对象(Class对象)
public static synchronized void method(){}
其他线程在获取到锁之前将被阻塞,必须等待锁的释放才能执行该方法
线程A 调用一个对象的非静态同步方法,线程B 调用同一对象的静态同步方法时,不会发生互斥,因为方法占用的锁不同。
- 修饰代码块:可以使用
synchronized
修饰代码块,指定需要同步的代码范围。synchronized
代码块 需要指定传入一个对象作为锁,只有持有该对象锁的线程才能执行该代码块,其他线程将被阻塞,需要等待锁的释放才能执行。
sychronized(obj){
// ...
}
一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized
方法,但是其他线程还是可以访问该对象的其他非 synchronized
方法。
线程A 在释放锁之前所有可见的共享变量,在 线程B 获取同一个锁之后,将立刻变得对 线程B 可见。
相关性质
synchronize优点
-
保证数据的一致性:
synchronized
禁止指令重排,可以确保多个线程对共享资源的访问是有序的、安全的、一致的,避免了数据的不一致性和错误。 -
实现线程的互斥访问:
synchronized
可以实现对共享资源的互斥访问,每次只允许一个线程执行同步代码块或同步方法,保证线程安全。 -
简化锁的管理:
synchronized
可以简化锁的管理,不需要手动地获取和释放锁,由 JVM 自动处理锁的申请和释放。 -
synchronized
是可重入锁,因此一个线程调用synchronized
方法的同时,可以在其方法体内部调用该对象另一个 synchronized 方法
缺点
-
如果临界区是只读操作,其实可以多线程一起执行,使用
synchronized
,同一时间只能有一个线程执行。 -
synchronized
无法知道线程有没有成功获取到锁。 -
使用
synchronized
,如果临界区因为IO
或者sleep
方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
使用规范
-
锁的粒度:应根据实际需求合理选择锁的粒度,过大的锁粒度可能导致性能问题,过小的锁粒度可能降低并发性能。
-
避免死锁:在使用多个锁的情况下,需要避免产生死锁的情况,即循环等待锁的发生,避免循环依赖和相互等待锁的情况。
-
锁的重入:同一个线程在拥有锁的情况下,可以重入同步代码,即可以再次获取相同锁。
-
精确控制同步范围:应尽量缩小同步代码块的范围,只对必要的操作进行同步,提高程序的并发性能。
-
锁的选择:根据实际需求,选择适当的锁对象,避免多个线程无意中使用了相同的锁对象,导致不必要的同步。
-
需要创建多个实例对象时,将 synchronized 作用于静态方法,防止不同的对象进入各自的对象锁,破坏线程安全。
通过合理使用 synchronized
关键字,可以确保多线程环境下数据的安全性,避免竞态条件和线程冲突,保证程序的正确性和稳定性。遵循synchronized
的规范和最佳实践,可以优化代码的性能和可维护性。
锁的存放位置
Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁。
每个 Java 对象都有一个对象头。
- 非数组类型,用 2 个字宽来存储对象头
- 数组,则会用 3 个字宽来存储对象头。
在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。对象头的内容如下表所示:
Mark Word
内容:
-
cms_free
是一个术语,可以与 CMS(Concurrent Mark Sweep)垃圾回收器相关联。CMS 是 Java 虚拟机中的一种并发标记清除垃圾收集器,用于在内存紧张的情况下减少应用程序的停顿时间。在 CMS 收集器的工作过程中,它使用了一组并发的线程来标记和清除不再使用的对象。
在 CMS 的工作过程中,当收集阶段完成,要回收的内存空间中的对象会被标记并释放。这个过程中会使用
cms_free
函数来释放对象所占用的内存。该函数会完成内存回收的操作,将对象标记为可用内存,并将其返回给堆内存供后续的对象分配使用。 -
hashcode
不是创建对象就帮我们写到对象头中的,而是要经过第一次调用Object::hashCode()
或者System::identityHashCode(Object)
才会存储在对象头中
synchronized 的锁状态
在 JDK 1.6 以前,所有的锁都是 重量级 锁,因为使用的是操作系统的互斥锁,当一个线程持有锁时,其他试图进入 synchronized块
的线程将被阻塞,直到锁被释放。涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低。
这也是很多开发者会认为 synchronized
性能很差的原因。
为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了 偏向锁 和 轻量级锁 的概念,对 synchronized
做了一次重大的升级,大大提升了性能。
在 JDK 1.6 及其以后,一个对象有四种锁状态,它们级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
无锁状态
无锁
状态(Unlocked State):当一个线程访问某个共享数据时,并没有其他线程同时访问该数据,那么这个共享数据处于无锁状态。此时,线程可以直接进行读写操作,无需进行同步。
无锁编程的出现主要是为了避免因高竞争锁而带来的性能开销。在高竞争场景下,全局锁的互斥操作可能会造成大量的线程切换和性能损失,甚至引发死锁和性能瓶颈。
无锁编程通过使用无锁算法来实现同步,允许多个线程同时访问同一数据结构,在无需锁的情况下进行读写操作,避免了锁带来的性能开销。
无锁编程通常使用原子操作来保证同步性,保证对共享数据的操作是原子性的,从而实现线程安全。
无锁编程的优势在于提高了程序的并发性能和可扩展性,但同时也需要考虑线程并发访问引起的冲突、需要考虑操作顺序的正确性等。
偏向锁状态
偏向锁
状态(Biased Locking State)是 Java HotSpot 虚拟机在实现锁机制时引入的一种锁状态,主要用于提高线程对同步锁的获取和释放性能。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
JDK 1.6 之后默认开启了偏向锁,但是开启有延迟,约为 4s。原因是 JVM 内部的代码有很多地方用到了 synchronized
,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗,所以就有了延迟策略。
可以通过参数 -XX:BiasedLockingStartupDelay
修改延迟
偏向锁给 JVM 增加了巨大的复杂性,维护成本较高。JDK 15 之前,偏向锁默认是 enabled,从 JDK 15 开始,默认就是 disabled,直接被 deprecated 了,除非显示的通过 UseBiasedLocking
开启。
实现原理
当一个线程访问某个同步块时,如果该同步块没有被其他线程占用,那么该线程会将锁的对象头信息中的标记位设置为偏向锁,并且把自己的线程 ID 记录在锁的对象头信息中。
在下一次该线程访问该同步块时,会直接尝试获取偏向锁,并检查该线程 ID 是否与锁对象头信息中记录的线程 ID 相同。
-
如果相同,直接获取锁,无需竞争、无需执行额外操作,在资源无竞争情况下消除了同步语句,大大提高了线程获取锁的性能。
-
如果不同,代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用
CAS
来替换 Mark Word 里面的线程 ID 为新线程的 ID,这个时候要分两种情况:-
成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
-
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
-
应用场景
偏向锁适用于多数情况下锁总是由同一个线程持有的场景,比如线程独享模式,这种情况下偏向锁能够显著地减少不必要的 CAS 操作
,从而提升了程序的性能和响应速度。
但对于多个线程竞争锁比较激烈的场景,则不适宜使用偏向锁,偏向锁会升级为重量级锁,增加竞争的激烈程度,反而会降低程序的性能表现。
深入偏向锁
匿名偏向状态
根据 JOL
(Java Object Layout)查看对象内存布局
结果中的 biasable
状态,在 MarkWord 表格中并不存在,这是一种匿名偏向状态,是对象初始化中,JVM 帮我们做的。当有线程进入同步块时:
-
biasable
:直接通过 CAS 替换 ThreadID,如果成功,可以获取偏向锁 -
non-biasable
:变成轻量级锁
Epoch
在 Mark Word 中,有 2bit 用于记录偏向锁的 Epoch
信息,其初始值为创建该对象时 class 中的 Epoch
的值。
Epoch
是时间的一个标记点,通常被用来记录时间戳的起始点。每个 class 对象会有一个对应的 Epoch
字段。
偏向锁撤销
偏向锁撤销和偏向锁释放是不同的概念,释放指的是 synchronized
方法的退出或 synchronized
代码块的结束
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向的撤销只能发生在有竞争的情况下,
偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,告知这个锁对象不能再用偏向模式,这个过程实际开销很大:
-
在一个安全点 safepoint(JVM 为了保证在垃圾回收的过程中引用关系不会发生变化设置的一种安全状态,在这个时间点上没有字节码正在执行)停止拥有锁的线程。
在这个安全点,线程可能还是处在不同的状态:
- 线程不存活,或者活着的线程退出了同步块,此时撤销偏向
- 活着的线程但仍在同步块之内,升级成轻量级锁
-
遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
-
唤醒被停止的线程,将当前锁升级成轻量级锁。
如果应用程序里所有的锁通常处于竞争状态,那么偏向锁就会是一种累赘,对于这种情况,可以一开始就把偏向锁这个默认功能给关闭:
-XX:UseBiasedLocking=false
![[acquisition and revocation of biased locks.png]]
批量重偏向
若 线程A 创建了大量对象并执行了初始的同步操作,之后在 线程B 中将这些对象作为锁进行之后的操作。此时,会导致大量的偏向锁撤销操作
为解决上述问题,引入了 批量重偏向
(bulk rebias):以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,只要 class 的对象发生偏向撤销,该计数器 +1
,当这个值达到重偏向阈值(默认 20)时:
BiasedLockingBulkRebiasThreshold = 20
JVM 认为该 class 的偏向锁有问题,会进行批量重偏向,
每次发生批量重偏向时,就将 epoch
值加 1,同时遍历 JVM 中所有线程的栈:
-
找到该 class 所有正处于加锁状态的偏向锁对象,将其
epoch
字段改为新值 -
class 中不处于加锁状态的偏向锁对象(没被任何线程持有,但之前是被线程持有过的,这种锁对象的 markword 肯定也是有偏向的),保持
epoch
字段值不变
这样下次获得锁时,发现当前对象的 epoch
值和 class 的 epoch
,本着今朝不问前朝事的原则(不考虑之前的 epoch
值),就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过 CAS 操作将其mark word 的 线程ID 改成当前 线程ID。
如果 epoch
都一样,说明没有发生过批量重偏向, 如果 markword
有线程 ID,还有其他锁来竞争,锁需要升级为轻量级锁。
批量撤销
当达到重偏向阈值后,假设该 class 计数器继续增长,当其达到批量撤销的阈值后(默认 40)时,
BiasedLockingBulkRevokeThreshold = 40
JVM 认为该 class 的使用场景存在多线程竞争,会标记该 class 为不可偏向。之后对于该 class 的锁,直接进行轻量级锁的逻辑。
在彻底禁用偏向锁之前,JVM 还会参考另一个计时器,来确定是否需要禁用偏向锁:
BiasedLockingDecayTime = 25000
-
如果在距离上次批量重偏向发生的 25 秒之内,并且累计撤销计数达到 40,就会发生批量撤销(偏向锁彻底禁止)
-
如果在距离上次批量重偏向发生超过 25 秒之外,就会重置在
[20, 40)
内的计数, 再次进入批量重偏向。
偏向锁的工作流程
偏向锁与 hashCode
在 Java 中,一个对象如果计算过 hashCode()
,就应一直保持不变,虽然用户可以重载 hashCode()
方法按照自己的意愿返回哈希码,但这样做很多依赖对象哈希码的 API 都可能存在出错风险。
作为绝大多数对象哈希码来源的 Object::hashCode()
方法,返回的是对象的一致性哈希码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码信永远不会再发生改变。
当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了,会直接使用轻量级锁。
而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。
在重量级锁的实现中,对象头指向了重量级锁的位置,什表重量级锁的ObiectMonitor
类里有字段可以记录非加锁状态(标志位为 01
)下的Mark Word,其中自然可以存储原来的哈希码。
偏向锁与 wait()
在同步块中调用 wait()
后,由于 wait()
是互斥量(重量级锁)独有的,一旦调用该方法,就会升级成重量级锁
轻量级锁状态
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。JVM 采用 轻量级锁
状态(Thin Locking State)来避免线程的阻塞与唤醒。用于提高低竞争情况下对同步锁的获取和释放性能。
实现原理
在轻量级锁状态下,当一个线程访问同步块时,虚拟机会将锁对象的对象头信息中的 Mark Word 设置为轻量级锁,并将锁对象头信息中的加锁标记指向当前线程堆栈帧中的锁记录(Lock Record)。
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,称为 Displaced Mark Word
。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word
复制到自己的 Displaced Mark Word
里面。
接着,虚拟机使用 CAS 操作
来尝试以原子方式将锁对象头的 Mark Word 指针替换为线程的锁记录指针:
-
若成功,表示线程成功获取锁
-
若失败,表示 Mark Word 已经被替换成了其他线程的锁记录,存在竞争线程,当前线程就尝试使用自旋(不断尝试去获取锁,一般用循环来实现)来获取锁。
自旋是需要消耗 CPU 的,如果一直获取不到锁的话,那该线程就一直处在自旋状态。因此 JDK 采用
适应性自旋
的方式,线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。
应用场景
轻量级锁的设计目标是在多个线程交替执行同一段同步代码时,减少传统重量级锁所需要的操作和开销,避免不必要的阻塞和唤醒操作,从而提高程序性能。相对于重量级锁,轻量级锁避免了线程切换和阻塞的开销,减少了锁的争用和同步操作的性能开销。
轻量级锁适用于短时间的互斥操作,例如同步块在多数情况下只会被一个线程多次访问的场景。线程可以通过轻量级锁快速地获取和释放锁,无需进行传统的互斥操作。
轻量级锁的释放
在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word
的内容复制回锁的 Mark Word
里面。
-
如果没有发生竞争,那么这个复制的操作会成功。
-
如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。
重量级锁状态
重量级锁
状态(Heavyweight Lockin State)是 Java HotSpot 虚拟机在实现锁机制时引入的一种锁状态,依赖于操作系统的互斥锁(mutex),用于在高竞争情况下保证线程的互斥性。
实现原理
当多个线程争用同一个锁时,轻量级锁的 CAS 操作就会不断重试,导致性能下降。此时,虚拟机会将轻量级锁升级为重量级锁,使用 monitor
实现同步机制。
Java 中,监视器(monitor)是一种同步工具,用于保护共享数据,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器。
重量级锁的设计目标是解决锁竞争量大的问题,提供一种高并发的同步机制,从而保证程序的正确性。
重量级锁采用互斥量的方式保证线程的互斥性,在多个线程访问共享资源,会产生锁竞争的情况下,线程会自动进行阻塞,并且进入到操作系统的内核态,进行等待和唤醒操作:
-
阻塞:当一个线程获取锁失败时,就会释放 CPU 的执行,在等待其他线程释放锁,并将自己挂起。被阻塞的线程不会消耗 CPU
-
唤醒:当其他线程释放锁时,会通知所有等待线程,唤醒其中一个线程,并将其从内核态重新调回到用户态,继续执行。
进入到内核状态,由操作系统来负责线程间的调度和线程的状态变更, 这就需要频繁的在这两个模式下切换(上下文切换)
应用场景
重量级锁适用于高竞争和长时间的互斥操作,例如多线程在竞争同一共享资源的场景。在该场景下,使用轻量级锁效率会变得很差,加重了线程之间的竞争,容易出现线程饥饿和锁饥饿的问题,因此使用重量级锁能够有效保证程序的正确性和性能。
多线程竞争锁
当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:
-
Contention List
:所有请求锁的线程将被首先放置到该竞争队列 -
Entry List
:Contention List 中那些有资格成为候选人的线程被移到 Entry List -
Wait Set
:那些调用 wait 方法被阻塞的线程被放置到 Wait Set -
OnDeck
:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck -
Owner
:获得锁的线程称为 Owner -
!Owner
:释放锁的线程
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个 ObjectWaiter
对象插入到 Contention List 队列的队首,然后调用 park()
方法挂起当前线程。
当线程释放锁时,会从 Contention List 或 EntryList 中挑选一个线程唤醒,被选中的线程叫做 Heir presumptive
即假定继承人,假定继承人被唤醒后会尝试获得锁,但 synchronized
是非公平的,所以假定继承人不一定能获得锁。
对于重量级锁,线程需要先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,并且自旋线程可能会抢占了 Ready
线程的锁。
如果线程获得锁后调用 Object.wait
方法,则会将线程加入到 WaitSet 中,当被 Object.notify
唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。
当调用一个锁对象的 wait
或 notify
方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
锁的升级与降级
锁的升级
sychronized
锁会随着竞争情况逐渐升级:
每一个线程在准备获取共享资源时:
-
检查 MarkWord 里面是不是放的自己的
ThreadId
,如果是,表示当前线程是处于偏向锁。 -
如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用 CAS 来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空。
-
两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作,把锁对象的 MarKword 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord。
-
第三步中成功执行 CAS 的获得资源,失败的则进入自旋 。
-
自旋的线程在自旋过程中,成功获得资源(即之前获得资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态
-
如果自旋失败。进入重量级锁的状态,此时,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
锁的降级
锁降级发生的条件比较苛刻了,锁降级发生在 Stop The World 期间,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。降级对象为仅仅能被 VMThread
访问而没有其他 JavaThread 访问的对象。
各级锁的优缺点
偏向锁和轻量级锁,都不会调用系统互斥量(Mutex Lock),它们只是为了提升性能多出来的两种锁状态,可以在不同场景下采取最合适的策略:
-
偏向锁:无竞争的情况下,只有一个线程进入临界区,采用偏向锁
-
轻量级锁:多个线程可以交替进入临界区,采用轻量级锁
-
重量级锁:多线程同时进入临界区,交给操作系统互斥量来处理
monitor
在 Java 中,monitor
(监视器)是一种同步机制来协调对共享资源的访问,synchronized 关键字就是基于 monitor 的实现。monitor
通过锁机制实现线程对共享资源的互斥访问,以保证线程安全。
当一个线程进入一个被 synchronized
修饰的代码块时,它尝试获取该代码块对应的 monitor 锁,在执行完代码块后再释放该锁。在该代码块持有 monitor
锁期间,其他线程无法访问该代码块所保护的共享资源,而会被阻塞直到 monitor
锁被释放。
monitor 还提供了 wait
、notify
和 notifyAll
三个方法,用于实现线程的协作:
-
wait()
:使当前线程进入等待状态,并释放锁,直到其他线程调用notify
或notifyAll
方法唤醒它; -
notify()
:唤醒在该 monitor 对象上等待的一个线程; -
notifyAll()
方法:唤醒在该 monitor 对象上等待的所有线程。
wait
和 notify
方法必须在同步代码块或同步方法中调用,并且调用对象必须是同步锁对象。当一个线程调用了 wait
方法后,它会释放当前的 monitor 锁,并进入等待状态。其他线程可以调用 notify
或 notifyAll
方法来唤醒等待的线程,并再次抢到 monitor
锁进入执行状态。
使用 monitor 可以避免线程之间的竞争和冲突,实现线程协作和共享资源的安全访问。但是,在使用 monitor 时需要注意避免死锁和饥饿等问题。