synchronized 是 Java 中保证线程安全的核心机制,其本质是通过 JVM 内置的 Monitor(监视器)实现互斥访问。当多个线程竞争同步资源时,synchronized 依靠对象头中的 Mark Word 和锁升级机制(偏向锁 → 轻量级锁 → 重量级锁)动态调整锁的实现方式,以平衡性能与线程安全。在字节码层面,synchronized 代码块通过 monitorenter 和 monitorexit 指令获取和释放锁,而 synchronized 方法则通过 ACC_SYNCHRONIZED 标志隐式加锁。除了互斥性,synchronized 还通过“happens-before”原则保证内存可见性:释放锁时将工作内存的修改刷新到主内存,获取锁时使本地缓存失效并重新读取主内存数据,从而确保线程间共享变量的最新值可见。常见使用场景包括保护共享资源、保证复合操作的原子性、实现单例模式的双重检查锁定以及配合 wait/notify 实现线程通信。性能方面,JVM 对低竞争场景下的偏向锁和轻量级锁优化显著,但在高竞争环境下可能因重量级锁导致线程阻塞和上下文切换开销增大,影响吞吐量。因此,应尽量减小锁粒度、避免死锁,并在高并发场景下权衡使用 ReentrantLock
synchronized关键字在 Java 中,在我看来,它本质上就是一把“锁”,一把确保同一时间只有一个线程能够访问特定代码区域或对象的锁。它通过 JVM 层面内置的监视器(Monitor)机制来实现互斥访问,同时,它还巧妙地保证了内存可见性,确保一个线程对共享变量的修改能被其他线程及时看到,从而有效避免了多线程环境下的数据不一致问题,是保证线程安全最直接、最基础的手段之一。
synchronized关键字的实现原理,说白了,就是围绕着 Java 对象头里的一个特殊结构——Monitor(监视器)来展开的。当我们使用
synchronized关键字修饰一个代码块或者一个方法时,实际上就是请求获取这个 Monitor 的所有权。
具体来说:
synchronized代码块: 当我们写
synchronized (this)或
synchronized (anObject)时,JVM 会在编译时生成
monitorenter和
monitorexit这两个字节码指令。
monitorenter指令:它尝试获取指定对象的 Monitor 锁。如果对象的 Monitor 计数器为 0,表示没有线程持有该锁,当前线程就能成功获取,然后将计数器加 1,并把 Monitor 的所有者设置为当前线程。如果计数器不为 0,说明有其他线程持有锁,当前线程就会被阻塞,直到持有锁的线程释放。
monitorexit指令:它会释放 Monitor 锁,将计数器减 1。当计数器减到 0 时,表示锁完全释放,其他等待的线程就有机会获取锁。值得注意的是,为了防止异常情况下锁无法释放,JVM 会在
monitorenter后面自动生成两个
monitorexit指令,一个在正常执行路径上,一个在异常处理路径上。
synchronized方法: 对于
synchronized修饰的实例方法或静态方法,JVM 不会显式地使用
monitorenter和
monitorexit指令。相反,它会在方法对应的
Constant Pool中设置一个
ACC_SYNCHRONIZED标志。当方法被调用时,JVM 会检查这个标志。如果设置了,执行线程就会自动尝试获取方法所属对象的 Monitor 锁(对于实例方法是实例对象,对于静态方法是类的 Class 对象)。方法执行完毕后,无论正常返回还是抛出异常,锁都会被自动释放。
无论是哪种形式,核心都是通过 Monitor 实现互斥。一个 Monitor 只能被一个线程持有,这确保了被
synchronized保护的代码块在任何时刻都只有一个线程在执行,从而防止了竞态条件(Race Condition)的发生。
synchronized关键字在 JVM 层面是如何具体实现的?
在我看来,
synchronized的实现远不止
monitorenter和
monitorexit那么简单,这背后其实藏着 JVM 对性能的极致优化和对并发编程复杂性的深刻理解。它的具体实现,与 Java 对象头中的
Mark Word息息相关。
Java 对象的内存布局通常包括对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。对象头又分为两部分:
Mark Word和
Klass Pointer。我们关注的重点是
Mark Word,它存储了对象的哈希码、GC 信息以及最重要的——锁信息。
JVM 为了提高
synchronized的性能,引入了锁升级(Lock Escalation)机制,主要经历了以下几个阶段:
Mark Word中记录下这个线程的 ID。如果后续该线程再次进入同步块,无需再进行任何同步操作,直接就可以执行。这就像给对象贴了个“专属标签”,只有你一个人用,就不用每次都检查门锁了。只有当有另一个线程尝试获取这个锁时,偏向锁才会撤销。
Lock Record,然后尝试使用 CAS(Compare And Swap)操作将对象的
Mark Word替换为指向
Lock Record的指针。如果成功,表示获取锁;如果失败,说明有其他线程也尝试获取,此时会膨胀为重量级锁。轻量级锁的优点是避免了操作系统级别的线程上下文切换,开销较小。
Mark Word会指向一个真正的
Monitor对象(通常是 C++ 实现的
ObjectMonitor),这个 Monitor 是在操作系统层面实现的。线程会被阻塞并挂起,进入等待队列,直到持有锁的线程释放锁。重量级锁的开销最大,因为它涉及到用户态到内核态的切换,以及线程的调度和上下文切换。
所以,
synchronized的实现原理,其实是一个动态调整的过程,JVM 会根据实际的竞争情况,在偏向锁、轻量级锁和重量级锁之间进行切换,力求在保证线程安全的前提下,最大化程序的性能。这真的是一个很精妙的设计。
synchronized如何保证内存可见性?
很多人提到
synchronized,首先想到的是它的互斥性,也就是“同一时间只有一个线程能访问”。但说实话,它的内存可见性保证同样重要,甚至在某些场景下更为关键。在并发编程中,内存可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。如果缺乏这个保证,即使有互斥,也可能因为线程读取到旧值而导致逻辑错误。
synchronized关键字通过 Java 内存模型(JMM)定义的“happens-before”原则来保证内存可见性。简单来说,
synchronized块的解锁操作
happens-before于后续对同一个
synchronized块的加锁操作。
具体机制是这样的:
synchronized锁时: 它会将自己在工作内存(线程私有缓存)中对所有共享变量的修改,全部刷新(flush)到主内存中。这就像是线程在离开一个共享工作区时,会把所有自己修改过的文件都保存到公共服务器上。
synchronized锁时: 它会强制性地使自己的工作内存中所有共享变量的缓存失效,然后从主内存中重新读取这些共享变量的最新值。这就像是线程进入共享工作区时,会先清空自己本地的旧文件,然后从公共服务器上下载最新的版本。
通过这种“先写回主内存,再从主内存读取”的机制,
synchronized确保了在一个线程执行完同步块并释放锁之后,其对共享变量的修改对后续获取相同锁的线程是可见的。这样,即使多个线程在不同的 CPU 核心上运行,也能保证它们看到的是共享变量的最新状态,从而避免了缓存不一致导致的可见性问题。
synchronized关键字有哪些使用场景和性能考量?
synchronized作为一个 JVM 内置的同步机制,在 Java 并发编程中有着不可替代的地位。了解它的使用场景和性能考量,能帮助我们更好地利用它。
使用场景:
synchronized最经典、最主要的应用。任何时候,只要有多个线程需要同时访问并修改一个共享变量、共享对象或数据结构(如
ArrayList、
HashMap等非线程安全的集合),都应该使用
synchronized来保护这些操作,以防止竞态条件导致的数据损坏或不一致。
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}synchronized可以确保这些步骤要么全部完成,要么全部不完成,中间不会被其他线程打断。
instance变量只被初始化一次,通常会使用
synchronized进行双重检查锁定(Double-Checked Locking)。
public class Singleton {
private volatile static Singleton instance; // volatile 保证可见性和禁止指令重排
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) { // 锁住 Class 对象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}wait(),
notify(),
notifyAll()方法不是
synchronized本身的功能,但它们必须在
synchronized块或方法内部调用,因为它们依赖于 Monitor 机制来管理线程的等待和唤醒。
性能考量:
synchronized的性能,尤其是重量级锁,确实是我们需要关注的。
synchronized的性能通常非常好,甚至可能比
java.util.concurrent.locks.ReentrantLock还要好,因为它避免了
ReentrantLock内部 CAS 操作的开销。
synchronized可能会成为性能瓶颈。
synchronized是一种独占锁,它限制了并发度。在高并发系统中,如果同步块成为瓶颈,可能会限制系统的整体吞吐量和可伸缩性。
总的来说,
synchronized是一个强大且易于使用的线程安全工具。在大多数中低并发场景下,它的性能表现是完全可以接受的,并且由于 JVM 的优化,很多时候甚至比手动实现的锁更高效。但在面对极高并发和复杂同步需求时,我们可能需要考虑
java.util.concurrent包下更灵活、功能更丰富的工具,比如
ReentrantLock、
StampedLock等,它们提供了更细粒度的控制和更高级的特性,比如公平锁、非阻塞尝试获取锁等。但无论如何,理解
synchronized的工作原理,都是我们掌握 Java 并发编程的基石。