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)机制,主要经历了以下几个阶段:
偏向锁(Biased Locking): 这是 JVM 默认开启的一种优化。当一个线程第一次访问同步块并获取锁时,JVM 会在
Mark Word
中记录下这个线程的 ID。如果后续该线程再次进入同步块,无需再进行任何同步操作,直接就可以执行。这就像给对象贴了个“专属标签”,只有你一个人用,就不用每次都检查门锁了。只有当有另一个线程尝试获取这个锁时,偏向锁才会撤销。轻量级锁(Lightweight Locking): 当偏向锁被撤销时,或者一开始就有多个线程竞争锁,但竞争不激烈(即没有线程阻塞),JVM 会升级为轻量级锁。线程会在自己的栈帧中创建一个
Lock Record
,然后尝试使用 CAS(Compare And Swap)操作将对象的
Mark Word
替换为指向
Lock Record
的指针。如果成功,表示获取锁;如果失败,说明有其他线程也尝试获取,此时会膨胀为重量级锁。轻量级锁的优点是避免了操作系统级别的线程上下文切换,开销较小。重量级锁(Heavyweight Locking): 当多个线程竞争激烈,或者轻量级锁 CAS 失败时,锁就会膨胀为重量级锁。此时,
Mark Word
会指向一个真正的
Monitor
对象(通常是 C++ 实现的
ObjectMonitor
),这个 Monitor 是在操作系统层面实现的。线程会被阻塞并挂起,进入等待队列,直到持有锁的线程释放锁。重量级锁的开销最大,因为它涉及到用户态到内核态的切换,以及线程的调度和上下文切换。
所以,
synchronized
的实现原理,其实是一个动态调整的过程,JVM 会根据实际的竞争情况,在偏向锁、轻量级锁和重量级锁之间进行切换,力求在保证线程安全的前提下,最大化程序的性能。这真的是一个很精妙的设计。
除了互斥,
synchronized
如何保证内存可见性?
很多人提到
synchronized
,首先想到的是它的互斥性,也就是“同一时间只有一个线程能访问”。但说实话,它的内存可见性保证同样重要,甚至在某些场景下更为关键。在并发编程中,内存可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。如果缺乏这个保证,即使有互斥,也可能因为线程读取到旧值而导致逻辑错误。
阿里云-虚拟数字人
阿里云-虚拟数字人是什么? …
2 查看详情
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
的性能,尤其是重量级锁,确实是我们需要关注的。
锁粒度: 锁的粒度越小,并发度越高。如果同步块包含了大量与共享资源无关的代码,那么就会不必要地阻塞其他线程,降低性能。所以,应该尽可能地缩小同步代码块的范围,只保护真正需要同步的部分。锁竞争:低竞争: 在低竞争场景下,由于 JVM 的偏向锁和轻量级锁优化,
synchronized
的性能通常非常好,甚至可能比
java.util.concurrent.locks.ReentrantLock
还要好,因为它避免了
ReentrantLock
内部 CAS 操作的开销。高竞争: 当多个线程频繁地竞争同一个锁时,锁会升级为重量级锁。此时,线程的阻塞和唤醒会涉及到操作系统层面的上下文切换,这会带来显著的性能开销。在高并发、高竞争的场景下,
synchronized
可能会成为性能瓶颈。死锁: 不恰当的锁顺序或嵌套锁可能导致死锁。一旦发生死锁,程序就会停滞不前,这是并发编程中最棘手的问题之一。可伸缩性:
synchronized
是一种独占锁,它限制了并发度。在高并发系统中,如果同步块成为瓶颈,可能会限制系统的整体吞吐量和可伸缩性。
总的来说,
synchronized
是一个强大且易于使用的线程安全工具。在大多数中低并发场景下,它的性能表现是完全可以接受的,并且由于 JVM 的优化,很多时候甚至比手动实现的锁更高效。但在面对极高并发和复杂同步需求时,我们可能需要考虑
java.util.concurrent
包下更灵活、功能更丰富的工具,比如
ReentrantLock
、
StampedLock
等,它们提供了更细粒度的控制和更高级的特性,比如公平锁、非阻塞尝试获取锁等。但无论如何,理解
synchronized
的工作原理,都是我们掌握 Java 并发编程的基石。
以上就是synchronized 关键字的实现原理是什么?它是如何保证线程安全的?的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/214219.html
微信扫一扫
支付宝扫一扫