深入理解 synchronized

深入理解 synchronized

引言:synchronized的核心地位

在Java并发编程中,synchronized关键字是实现线程安全的基石。自JDK 1.0引入以来,它经历了从"重量级锁"到"自适应锁"的进化,如今已成为兼顾安全性与性能的成熟方案。本文将从用法解析字节码实现底层原理锁升级机制JDK优化性能对比最佳实践,全方位剖析synchronized的技术细节,结合OpenJDK源码与实测数据,带你彻底掌握这一并发利器。

一、synchronized的基本用法与语义

1.1 三种使用方式

synchronized可修饰方法或代码块,核心是通过对象锁实现线程互斥。具体用法如下:

用法场景锁对象字节码实现示例代码
修饰实例方法当前对象实例(this方法访问标志ACC_SYNCHRONIZEDpublic synchronized void increment() { count++; }
修饰静态方法类对象(Class实例)方法访问标志ACC_SYNCHRONIZEDpublic static synchronized void staticIncrement() { staticCount++; }
修饰代码块显式指定对象monitorenter/monitorexit指令synchronized (lockObj) { count++; }

关键语义

  • 互斥性:同一时刻只有一个线程能持有锁,确保临界区代码串行执行。
  • 可见性:释放锁时,线程会将工作内存中的修改刷新到主内存;获取锁时,线程会失效本地缓存,从主内存加载最新值(通过内存屏障实现)。
  • 可重入性:线程可重复获取已持有的锁,通过_recursions计数器实现(见ObjectMonitor源码)。

1.2 用法示例与字节码分析

示例1:同步代码块
public class SyncBlockExample {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() {
        synchronized (lock) { // 显式指定lock为锁对象
            count++;
        }
    }
}

字节码反编译javap -v SyncBlockExample.class):
同步代码块通过monitorenter(进入锁)和monitorexit(释放锁)指令实现:

  public void increment();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #2                  // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_1                          // 将锁对象引用存入局部变量表
         6: monitorenter                      // 获取锁
         7: aload_0
         8: dup
         9: getfield      #3                  // Field count:I
        12: iconst_1
        13: iadd
        14: putfield      #3                  // Field count:I
        17: aload_1
        18: monitorexit                       // 正常退出时释放锁
        19: goto          27
        22: astore_2
        23: aload_1
        24: monitorexit                       // 异常退出时释放锁
        25: aload_2
        26: athrow
        27: return

注意:编译器会生成两个monitorexit,分别对应正常退出和异常退出,确保锁必定释放。

示例2:同步方法
public class SyncMethodExample {
    private int count = 0;

    public synchronized void increment() { // 实例方法锁,锁对象为this
        count++;
    }

    public static synchronized void staticIncrement() { // 静态方法锁,锁对象为SyncMethodExample.class
        staticCount++;
    }
}

字节码特征:同步方法通过ACC_SYNCHRONIZED标志实现,无需显式monitor指令:

  public synchronized void increment();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 同步方法标志
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return

二、底层实现:对象头与Monitor机制

2.1 对象头与Mark Word

synchronized的实现依赖对象头(Object Header)中的Mark Word存储锁状态。对象头由两部分组成:

  • Mark Word:存储对象运行时数据(哈希码、GC年龄、锁状态等)。
  • Klass Pointer:指向类元数据的指针。

64位JVM Mark Word格式(不同锁状态下的存储结构):

锁状态标志位存储内容
无锁01哈希码(25bit)+ GC年龄(4bit)+ 是否偏向锁(1bit=0)+ 标志位(2bit=01)
偏向锁01偏向线程ID(54bit)+ Epoch(2bit)+ GC年龄(4bit)+ 是否偏向锁(1bit=1)+ 标志位(2bit=01)
轻量级锁00指向栈中锁记录(Lock Record)的指针(64bit)+ 标志位(2bit=00)
重量级锁10指向ObjectMonitor对象的指针(64bit)+ 标志位(2bit=10)
GC标记11

工具推荐:使用org.openjdk.jol:jol-core查看对象头,如ClassLayout.parseInstance(obj).toPrintable()

2.2 Monitor监视器锁

重量级锁的实现依赖ObjectMonitor(C++实现),每个对象关联一个Monitor,用于管理线程竞争与等待。

ObjectMonitor核心结构(OpenJDK源码objectMonitor.hpp):

class ObjectMonitor {
private:
    void* volatile _owner;       // 持有锁的线程
    ObjectWaiter* volatile _WaitSet; // 等待队列(调用wait()的线程)
    ObjectWaiter* volatile _EntryList; // 阻塞队列(未获取锁的线程)
    int _recursions;             // 重入次数
    int _count;                  // 等待线程数
    // ...其他字段
};

工作流程

  1. 竞争锁:线程通过CAS尝试将_owner设为自身,成功则获取锁;失败则进入_EntryList阻塞。
  2. 释放锁:线程退出同步块时,将_owner设为null,唤醒_EntryList中的线程重新竞争。
  3. 等待/唤醒:调用wait()时,线程释放锁并进入_WaitSetnotify()将线程从_WaitSet移至_EntryList重新竞争。

三、锁升级机制:从偏向锁到重量级锁

JDK 1.6引入锁升级机制,根据竞争程度动态选择锁状态(不可逆),平衡性能与安全性。

3.1 偏向锁(Biased Locking)

设计目标:减少单线程重复获取锁的开销。
实现原理

  • 首次获取锁时,通过CAS将线程ID记录到Mark Word,设为偏向模式(标志位101)。
  • 后续同一线程访问时,仅需比对线程ID,无需CAS操作。

撤销条件:当其他线程尝试竞争时,需等待全局安全点(STW),暂停持有线程,检查状态:

  • 若持有线程已结束,重置为无锁状态。
  • 若持有线程存活,升级为轻量级锁。

JVM参数

  • -XX:+UseBiasedLocking(默认启用,JDK 15后默认禁用)。
  • -XX:BiasedLockingStartupDelay=0(禁用启动延迟)。

3.2 轻量级锁(Lightweight Locking)

设计目标:应对多线程交替执行的轻度竞争。
实现步骤

  1. 创建锁记录:线程在栈帧中创建Lock Record,复制Mark Word(Displaced Mark Word)。
  2. CAS竞争锁:通过CAS将Mark Word替换为指向Lock Record的指针(标志位00)。
  3. 自旋重试:竞争失败时,线程自旋(空循环)尝试获取锁,避免阻塞(自适应自旋:根据历史成功率调整次数)。

升级条件

  • 自旋超过阈值(默认10次,JDK 1.7后自适应)。
  • 竞争线程数超过CPU核心数一半。

3.3 重量级锁(Heavyweight Locking)

设计目标:应对高并发激烈竞争。
实现原理

  • Mark Word指向ObjectMonitor,未获取锁的线程进入_EntryList阻塞(操作系统级别的互斥锁)。
  • 线程阻塞/唤醒涉及用户态→内核态切换,开销较大。

性能对比

锁状态获取成本释放成本适用场景
偏向锁极低极低单线程重复访问
轻量级锁低(CAS)低(CAS)多线程交替执行
重量级锁多线程同时竞争

四、JDK优化:从锁消除到虚拟线程

4.1 锁优化技术

锁消除(Lock Elimination)

JIT编译器通过逃逸分析,消除不可能存在竞争的锁。例如:

public String concat(String a, String b) {
    StringBuffer sb = new StringBuffer(); // StringBuffer的append是同步方法
    sb.append(a).append(b);
    return sb.toString();
}
// 逃逸分析发现sb未逃逸,消除同步锁
锁粗化(Lock Coarsening)

合并连续的锁申请,减少锁竞争频率:

for (int i = 0; i < 1000; i++) {
    synchronized (lock) { // 循环内频繁加锁,粗化为一次锁申请
        count++;
    }
}
// 优化后:synchronized (lock) { for (...) { count++; } }
自适应自旋(Adaptive Spinning)

JVM根据历史自旋成功率动态调整次数:

  • 若自旋成功,下次增加自旋次数(最大100次)。
  • 若自旋失败,减少或省略自旋,避免CPU空转。

4.2 JDK 17的虚拟线程支持

JDK 17通过JEP 491优化synchronized与虚拟线程(Virtual Threads)的兼容性,避免线程固定(Pinning):

  • 虚拟线程阻塞于synchronized时,JVM自动卸载载体线程(Carrier Thread),允许其他虚拟线程复用。
  • 实现原理:结合Continuation机制,在阻塞时保存栈帧,释放载体线程。

性能提升:在高并发I/O场景,吞吐量提升30%+,避免传统线程阻塞导致的资源浪费。

五、源码深度剖析:ObjectMonitor关键方法

5.1 加锁(enter方法)

void ATTR ObjectMonitor::enter(TRAPS) {
    Thread* Self = THREAD;
    void* cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL); // CAS尝试获取锁
    if (cur == NULL) { 
        // 成功获取锁,_owner = Self
        return;
    }
    if (cur == Self) { 
        // 重入,_recursions++
        _recursions++;
        return;
    }
    // 竞争失败,进入自旋或阻塞
    if (Knob_SpinEarly && TrySpin(Self) > 0) { 
        // 自旋成功,获取锁
        return;
    }
    // 自旋失败,进入_EntryList阻塞
    ThreadBlockInVM tbivm(Self);
    Self->set_current_pending_monitor(this);
    EnterI(Self); // 进入阻塞队列
}

5.2 释放锁(exit方法)

void ATTR ObjectMonitor::exit(TRAPS) {
    if (_recursions != 0) { 
        // 重入解锁,_recursions--
        _recursions--;
        return;
    }
    // 唤醒_EntryList中的线程
    ObjectWaiter* w = NULL;
    w = _EntryList;
    if (w != NULL) {
        Atomic::cmpxchg_ptr(NULL, &_owner, Self); // 释放锁
        OrderAccess::fence();
        WakeupWaiter(w); // 唤醒线程
        return;
    }
    // 无等待线程,直接释放
    Atomic::cmpxchg_ptr(NULL, &_owner, Self);
}

六、性能对比与最佳实践

6.1 synchronized vs Lock(ReentrantLock)

特性synchronizedReentrantLock
实现层级JVM层面(关键字)JDK层面(接口)
锁释放自动释放(异常/正常退出)手动释放(需finally块)
高级功能无(不可中断、非公平)支持中断、超时、公平锁、条件变量
性能(低竞争)接近(偏向锁/轻量级锁)略高(CAS操作)
性能(高竞争)重量级锁开销大略优(队列优化)

建议:简单同步需求用synchronized(简洁安全);需高级功能(如超时获取)用ReentrantLock

6.2 生产环境最佳实践

  1. 减小锁粒度:同步代码块仅包裹临界区,避免锁范围过大:

    // 反例:整个方法加锁
    public synchronized void process() {
        readConfig(); // 无需同步的操作
        updateState(); // 需同步的临界区
    }
    // 正例:仅临界区加锁
    public void process() {
        readConfig();
        synchronized (lock) {
            updateState();
        }
    }
    
  2. 避免嵌套锁:减少死锁风险,如必须嵌套,确保锁顺序一致。

  3. 禁用偏向锁:高并发场景下,偏向锁撤销开销大,通过-XX:-UseBiasedLocking禁用。

  4. 监控锁状态:使用jstack查看线程阻塞状态,定位竞争热点:

    jstack <pid> | grep -A 20 "BLOCKED"  # 查看阻塞线程
    
  5. JVM参数调优

    • -XX:PreBlockSpin=10:轻量级锁自旋次数。
    • -XX:BiasedLockingBulkRebiasThreshold=20:批量重偏向阈值。
    • -XX:BiasedLockingBulkRevokeThreshold=40:批量撤销阈值。

七、总结

synchronized从早期的重量级锁进化为如今的自适应锁机制,体现了JVM对性能的极致追求。其核心在于动态锁升级(偏向锁→轻量级锁→重量级锁),结合对象头Mark Word与ObjectMonitor实现高效同步。在JDK 17中,通过对虚拟线程的支持,进一步提升了高并发场景下的可扩展性。

掌握synchronized的底层原理,不仅能写出更高效的并发代码,更能深入理解JVM的优化机制。在实际开发中,需结合业务场景选择合适的锁策略,平衡安全性与性能,避免过度同步或锁竞争导致的性能瓶颈。

参考资料

  • OpenJDK源码(hotspot/src/share/vm/runtime/objectMonitor.hpp)
  • 《深入理解Java虚拟机》(周志明)
  • JDK官方文档(JEP 142、JEP 491)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值