[面试题] synchronized 是如何保证原子性、可见性、有序性的?
📖 版块 A:知识讲解版 (温故知新)
- 原子性 (Atomicity):
synchronized通过 JVM 层面的monitorenter和monitorexit指令实现。当一个线程进入同步代码块时,必须先获取对象的 Monitor 锁。在持有锁期间,其他线程无法进入该代码块,从而保证了这一系列操作是不可分割的整体,实现了互斥访问。 - 可见性 (Visibility):根据 Java 内存模型 (JMM) 的规范,
synchronized的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中”以及“对一个变量执行 lock 操作时,将会清空工作内存中此变量的值,在执行引擎使用前,需要重新从主内存中 load 或 assign”这两条规则共同保证的。简单说,就是“加锁前清缓存,释放锁前刷主存”。 - 有序性 (Orderliness):
synchronized的有序性体现在“虽然它不能禁止代码块内部的指令重排序(仍需遵循 as-if-serial 语义),但由于它保证了同一时刻只有一个线程执行该代码块,使得多个线程之间的执行顺序变成了串行化”。在单线程环境下,指令重排不会改变执行结果,因此从外部观察,整个同步块的执行结果是符合逻辑顺序的。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:synchronized 是如何保证原子性、可见性、有序性的?
💬 高手逐字稿:
关于 synchronized 保证并发三要素的原理,我可以从底层的实现机制和 JMM 规范两个维度来回答。
首先是原子性。synchronized 是通过 JVM 指令集中的 monitorenter 和 monitorexit 来实现的。当线程执行到同步块时,会尝试获取 Monitor 对象的锁。一旦获取成功,计数器加 1,在这个过程中,其他线程无法获取同一把锁,只能在同步队列中等待。这种互斥性保证了同步块内的代码要么不执行,执行了就一定能执行完且不被干扰,从而实现了宏观上的原子性。
其次是可见性。这主要归功于 Java 内存模型中关于“同步块”的内存屏障语义。JMM 规定,在释放锁(unlock)之前,必须把工作内存中的共享变量刷新回主内存;而在获取锁(lock)之后,必须清空工作内存,重新从主内存中读取。这意味着,一个线程对变量的修改,在它释放锁的那一刻,对后续获取同一把锁的线程是绝对可见的。
最后是有序性。这里有一个细节:synchronized 其实并不禁止同步块内部的代码进行指令重排序。但是,JMM 有一条 Happens-Before 规则:对同一个锁的 unlock 操作先行发生于后面对这个锁的 lock 操作。由于 synchronized 保证了同一时间只有一个线程执行,根据 as-if-serial 语义,单线程内的重排序不会影响最终结果。所以,虽然内部可能有乱序,但在多线程视角下,同步块的执行表现出了天然的有序性。
这里可以深入讨论一个细节:很多人会把 synchronized 的有序性和 volatile 的有序性混淆。实际上,volatile 是通过内存屏障直接禁止了指令重排;而 synchronized 则是通过“互斥锁”将并发执行转变为串行执行,从而规避了乱序带来的安全问题。
所以,在实际开发中,如果我们要保证复合操作的完整性,必须用 synchronized;如果只是简单的状态同步,volatile 的开销会更低。