synchronized
是 Java 中用于实现线程同步、保证多线程安全、访问共享资源的关键字。它是 Java 提供的一种内置的同步机制,基于管程 (Monitor) 的概念。
1. synchronized
关键字的作用是什么?
在多线程环境中,当多个线程同时访问或修改同一个共享变量或资源时,可能会出现以下问题:
- 原子性问题 (Atomicity): 一个看似简单的操作(如
i++
)在底层可能分解为多个指令(读取 i,i 加 1,写入 i)。如果多个线程同时执行这个操作,这些子指令可能会交错执行,导致最终结果错误。 - 可见性问题 (Visibility): 一个线程对共享变量的修改可能只存在于其自身的工作内存(如 CPU 缓存)中,而没有及时写回主内存。其他线程从主内存读取到的仍然是旧值,无法“看见”最新的修改。
- 有序性问题 (Ordering): 为了优化性能,编译器和处理器可能会对指令进行重排序。在单线程中,重排序一般不会影响到最终结果。但在多线程环境中,指令重排序可能会导致意料之外的结果。
synchronized
关键字主要用于解决这些问题,它提供互斥访问和内存同步的功能:
- 互斥性 (Mutual Exclusion):
synchronized
确保在同一时刻,最多只有一个线程能够进入由它修饰的同步代码块或方法。这保证了被保护的代码是原子执行的,不会被其他线程的操作打断,从而解决了原子性问题和潜在的数据竞争。 - 可见性 (Visibility): 当一个线程进入
synchronized
代码块或方法时,它会使自己的工作内存失效,强制从主内存中读取共享变量的最新值。当一个线程退出synchronized
代码块或方法时,它会把工作内存中的共享变量的修改刷新到主内存。这保证了线程之间对共享变量修改的可见性。 - 有序性 (Ordering):
synchronized
的互斥和内存同步机制(工作内存与主内存的数据传输)天然地保证了操作的有序性,遵循 JMM 的管程锁定规则 (Monitor Lock Rule): 对一个管程(monitor)的解锁操作 happens-before 随后对这个管程的加锁操作。这确保了在前一个线程释放锁时所做的所有修改,在后一个线程获取锁后都能被看见,并且后一个线程在锁内执行的操作不会被重排序到获取锁之前。
总之,synchronized
关键字是用于保证共享资源在多线程环境下的安全访问,通过提供互斥和内存同步来同时解决并发中的原子性、可见性和有序性问题。
2. synchronized
是如何实现的?
synchronized
的实现依赖于 JVM 内部的对象监视器 (Monitor)。每个 Java 对象都可以关联一个监视器。当线程执行到同步代码时,它会尝试获取对象的监视器。
synchronized
的用法有两种形式:
-
同步方法:
public synchronized void method() { // 同步代码 } public static synchronized void staticMethod() { // 同步代码 }
- 对于实例方法,锁住的是当前对象实例 (
this
) 的监视器。 - 对于静态方法,锁住的是当前类的Class 对象 (
类名.class
) 的监视器。
- 对于实例方法,锁住的是当前对象实例 (
-
同步代码块:
public void anotherMethod() { // ... 一些非同步代码 synchronized (object) { // 锁住指定的 object 对象的监视器 // 同步代码 } // ... 一些非同步代码 }
- 锁住的是括号中指定对象的监视器。这个对象被称为监视器对象或锁对象。
底层实现机制:
在 JVM 字节码层面,synchronized
的实现方式有所不同:
- 同步方法: JVM 会在方法的访问标志中设置
ACC_SYNCHRONIZED
标志。当线程调用带有此标志的方法时,JVM 会隐式的执行monitorenter
和monitorexit
指令序列,对方法对应的对象(实例方法是对象实例,静态方法是 Class 对象)进行加锁和解锁操作。 - 同步代码块: 这是通过显式地使用
monitorenter
和monitorexit
字节码指令来实现的。monitorenter
:当线程执行到monitorenter
指令时,它会尝试获取栈顶对象(即同步代码块括号中的对象)的监视器。如果对象的监视器没有被其他线程持有,当前线程就会持有该监视器,然后计数器加 1。如果该对象的监视器已被当前线程持有(重入),则只将计数器加 1。如果该对象的监视器被其他线程持有,当前线程就会阻塞,直到获取监视器。monitorexit
:当线程执行到monitorexit
指令时,它会释放当前线程持有的对象的监视器。监视器的计数器减 1。如果计数器归零,监视器就被完全释放,其他等待的线程就有机会获取该监视器。
为了确保无论同步代码块是否发生异常,监视器都能被释放,编译器会在同步代码块的末尾自动生成一个 finally
块,并在 finally
块中再插入一个 monitorexit
指令。
对象头与监视器:
在 HotSpot JVM 中,对象的监视器信息通常存储在对象头的Mark Word 中。Mark Word 是对象头的一部分,用于存储对象的运行时数据,如哈希码、分代年龄、锁标志位等。当对象被用作锁对象时,Mark Word 中的某些位会用来表示锁的状态(无锁、偏向锁、轻量级锁、重量级锁等),指向锁记录或监视器指针。
锁升级/锁膨胀 (Lock Escalation):
最新版本中JVM 对 synchronized
进行了大量的优化,以降低其性能开销。它不是一开始就使用重量级的操作系统互斥锁,而是会根据竞争情况逐渐升级锁的状态:
- 无锁状态: 对象刚创建时处于无锁状态。
- 偏向锁 (Biased Locking): 如果同一个线程多次获取同一个锁,JVM 会自动进入偏向锁模式。对象的 Mark Word 会记录下偏向的线程 ID。下次该线程再次尝试获取锁时,只需要检查 Mark Word 中的线程 ID 是否是自己,而无需执行额外的同步操作,大大降低了开销。如果另一个线程尝试获取这个锁,偏向锁就会撤销 (revoke),升级为轻量级锁。
- 轻量级锁 (Lightweight Locking): 当偏向锁失效或者多个线程交替访问但没有竞争时,会升级为轻量级锁。线程会在自己的栈帧中创建锁记录 (Lock Record),并尝试使用 CAS (Compare-And-Swap) 操作将对象的 Mark Word 更新为指向栈帧中的锁记录。如果 CAS 成功,表示获取锁;如果失败,表示存在竞争,会升级为重量级锁。轻量级锁是基于忙等待 (spinning) 的,不会阻塞线程,适用于锁持有时长短且竞争不激烈的场景。
- 重量级锁 (Heavyweight Locking): 如果 CAS 失败(存在竞争),或者锁的持有时长较长,就会升级为重量级锁。重量级锁是基于操作系统底层的互斥锁 (Mutex) 实现的。线程获取重量级锁失败时,会被操作系统挂起(阻塞),让出 CPU,直到锁被释放并被唤醒。重量级锁开销较大,因为它涉及用户态和内核态之间的切换以及线程的上下文切换。
通过这种锁升级机制,JVM 在大部分非竞争或低竞争场景下都能以较低的开销实现同步,只有在高竞争时才会使用开销较大的重量级锁。
总结:
synchronized
通过对象监视器实现互斥和内存同步,保证了多线程对共享资源的原子性、可见性和有序性访问。其底层实现涉及字节码指令(monitorenter
、monitorexit
)、对象头的 Mark Word 以及JVM 的锁升级优化(偏向锁、轻量级锁、重量级锁),以在不同竞争程度下提供合适的性能。