synchronized 基础与原理全解析

一、概述

synchronized 是 Java 内置的关键字,用于实现线程同步。在并发编程中,它能保证原子性可见性有序性,是最常用的同步手段之一。

JDK1.6 之后,JVM 对 synchronized 做了大量优化(如偏向锁、轻量级锁、自旋、锁消除等),性能已经有了极大提升,因此在实际开发中完全可以放心使用。


二、synchronized 的语义

  1. 互斥性
    确保同一时间只有一个线程能进入同步块或同步方法。

  2. 可见性
    线程在释放锁时,会把本地工作内存中的数据刷新到主内存;获取锁的线程会从主内存中重新读取数据。

  3. 有序性
    JVM 在进入与退出同步块时,会插入内存屏障,禁止指令重排序,保证临界区内外的执行顺序。


三、实现基础

  1. 对象头与 Mark Word
    HotSpot 虚拟机中,每个对象都有一个对象头。对象头中的 Mark Word 用于存储:

    • 哈希值(HashCode)
    • GC 年龄
    • 锁状态(无锁、偏向锁、轻量级锁、重量级锁)
      锁的升级,本质就是 Mark Word 的变化。
  2. Monitor 与字节码指令

    • 同步代码块:编译成 monitorenter / monitorexit 指令。
    • 同步方法:通过方法的 ACC_SYNCHRONIZED 标志实现。
    • Monitor 内部保存了锁的持有者和重入次数,因此 synchronized可重入锁
      在这里插入图片描述

四、锁的升级过程

  1. 无锁状态:对象未被任何线程加锁。
  2. 偏向锁:无竞争情况下,锁偏向第一个获取它的线程。再次进入时只需校验线程 ID,无需 CAS。若其他线程竞争,偏向锁会被撤销。
  3. 轻量级锁:当多个线程竞争时,尝试使用 CAS 将对象头的 Mark Word 指向线程栈中的 Lock Record。失败后进入自旋等待。
  4. 重量级锁:竞争激烈时,自旋失败,锁会膨胀为重量级锁,线程阻塞,依赖操作系统调度。

升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(一般不可逆)。


五、轻量级锁的加锁与解锁

1. 加锁过程

  1. 当线程进入同步块时,JVM 在当前线程的栈帧中创建一份锁记录(Lock Record),保存对象头中 Mark Word 的拷贝,称为 Displaced Mark Word

  2. 尝试使用 CAS 将对象头中的 Mark Word 替换为指向 Lock Record 的指针,同时在 Lock Record 中记录原始 Mark Word。

  3. 如果 CAS 成功,线程获取轻量级锁,对象进入轻量级锁定状态。

  4. 如果失败:

    • 检查对象头是否已经指向当前线程的栈帧,若是则说明线程重入,可以直接进入;
    • 否则说明有竞争,线程会进行自旋,若多次 CAS 失败,则升级为重量级锁

2. 解锁过程

  1. 通过 CAS 尝试将 Displaced Mark Word 恢复到对象头。
  2. 如果 CAS 成功,解锁完成。
  3. 如果 CAS 失败,说明锁已经升级为重量级锁,此时需要在释放时通知其他等待线程重新竞争。

总结:轻量级锁通过 CAS 和自旋来避免阻塞,性能优于重量级锁。
在这里插入图片描述


六、等待与通知机制

  1. wait():当前线程释放锁,进入等待状态,直到被唤醒。

  2. notify():唤醒一个正在等待的线程。

  3. notifyAll():唤醒所有等待线程,让它们重新竞争锁。

  4. 注意事项

    • wait/notify 必须在同步块中调用,否则会抛 IllegalMonitorStateException
    • 推荐用 while 循环检查条件,避免伪唤醒。

七、JVM 对 synchronized 的优化

  1. 锁消除
    编译器在 JIT 阶段发现锁对象不会被共享时,会直接移除无用的同步。

  2. 锁粗化
    如果多个连续的同步块作用于同一个对象,会合并成一个更大的同步块,减少加解锁次数。

  3. 自适应自旋
    当线程获取轻量级锁失败时,JVM 会先自旋等待一段时间。如果等待期间锁被释放,就可以避免线程阻塞。自旋时间会根据历史情况自适应调整。

  4. 批量撤销偏向锁
    当某个类的对象频繁发生锁竞争,JVM 会关闭该类的偏向锁,或在安全点进行批量撤销,减少偏向锁的撤销开销。


八、synchronized 的使用方式

  1. 修饰实例方法:锁的是当前对象实例。

    public synchronized void foo() { ... }
    
  2. 修饰静态方法:锁的是当前类的 Class 对象。

    public static synchronized void bar() { ... }
    
  3. 同步代码块:锁的是括号中指定的对象。

    synchronized (lock) { ... }
    

九、与 ReentrantLock 的对比

特性synchronizedReentrantLock
实现JVM 关键字JDK API
公平锁不支持支持
可中断不支持支持
条件队列单一(wait/notify)多条件(Condition)
锁释放自动释放必须手动 unlock
优化偏向、轻量级、自旋依赖实现

结论:简单场景优先用 synchronized,需要更灵活的功能时使用 ReentrantLock


十、常见问题与注意点

  1. 异常是否释放锁?
    会。线程退出同步块(包括异常)时,锁都会被释放。

  2. 锁能否降级?
    一般不能。轻量级锁升级到重量级锁后不会回退。

  3. wait 是否释放锁?
    会,wait() 会释放锁并进入等待,唤醒后重新竞争锁。

  4. 锁对象选择
    推荐使用 private final Object lock = new Object();。不要使用 this 或公共对象,避免意外竞争和死锁。


十一、总结

synchronized 是 Java 并发的基石。
它通过 对象头 Mark Word + Monitor 实现锁机制,并通过 偏向锁 → 轻量级锁 → 重量级锁 的升级路径来优化性能。轻量级锁利用 CAS 和自旋避免了阻塞,重量级锁则保证了高竞争场景下的稳定性。

理解其加解锁原理、等待通知机制、以及 JVM 的优化策略,能帮助我们写出更高效、更健壮的并发代码。在简单场景下,synchronized 足够安全可靠;在复杂场景下,可以结合 ReentrantLock 提供更灵活的控制。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值