Java:Synchronized底层实现详解

Java:Synchronized 底层实现详解

在这里插入图片描述

在 Java 并发编程中,synchronized是保证线程安全的基础手段,也是 JVM 层面提供的内置同步机制。从 JDK 1.0 诞生至今,它经历了多次重大优化,从最初的 “重量级锁” 演变为如今具备锁升级能力的高效同步工具。本文将从字节码解析、JVM 实现、硬件交互三个维度,全方位剖析synchronized的底层工作原理,结合实战案例说明其优化机制与最佳实践。

一、Synchronized 基础认知与使用场景

1.1 核心功能与特性

synchronized的核心作用是实现线程间的互斥访问,确保同一时间只有一个线程能执行特定代码块,从而保证并发场景下的:

  • 原子性:临界区操作不可分割,要么全部执行,要么全部不执行
  • 可见性:锁释放时会将工作内存中的修改刷新到主内存,确保其他线程可见
  • 有序性:禁止指令重排序优化,保证代码执行顺序与预期一致

1.2 三种使用形式及锁对象分析

(1)修饰实例方法

锁对象为当前对象实例(this),所有调用该方法的线程会竞争同一个实例的锁。

public class SyncDemo {
    // 锁对象为当前实例(this)
    public synchronized void instanceMethod() {
        // 临界区代码
        System.out.println("实例方法同步块");
    }
}
(2)修饰静态方法

锁对象为当前类的Class对象(全局唯一),所有线程共享同一把锁。

public class SyncDemo {
    // 锁对象为SyncDemo.class
    public static synchronized void staticMethod() {
        // 临界区代码
        System.out.println("静态方法同步块");
    }
}
(3)修饰代码块

需显式指定锁对象,可灵活控制同步粒度。

public class SyncDemo {
    private final Object lock = new Object(); // 自定义锁对象
    
    public void blockMethod() {
        // 锁对象为lock实例
        synchronized (lock) {
            // 临界区代码
            System.out.println("代码块同步");
        }
        
        // 锁对象为当前类的Class对象
        synchronized (SyncDemo.class) {
            // 临界区代码
        }
    }
}

锁对象的关键特性

  • 必须是引用类型(不能是intlong等基本类型)
  • 建议使用private final修饰,避免锁对象被意外修改导致同步失效
  • 不同锁对象之间的同步互不干扰,可实现细粒度控制

二、字节码层面的 Synchronized 实现

synchronized在编译后的字节码中表现形式不同,代码块和方法级别的同步有着不同的实现方式。

2.1 同步代码块的字节码解析

对同步代码块进行编译后,会生成monitorentermonitorexit两条核心指令:

public class SyncBytecodeDemo {
    private final Object lock = new Object();
    
    public void syncBlock() {
        synchronized (lock) {
            System.out.println("同步代码块");
        }
    }
}

使用javap -v SyncBytecodeDemo.class查看字节码(简化版):

public void syncBlock();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=1
       0: aload_0         // 加载当前对象引用
       1: getfield        #2  // 获取lock字段的值
       4: dup             // 复制lock对象引用
       5: astore_1        // 将lock引用存储到局部变量表索引1
       6: monitorenter    // 进入同步块,获取锁
       7: getstatic       #3  // 访问System.out
      10: ldc             #4  // 加载字符串"同步代码块"
      12: invokevirtual   #5  // 调用println方法
      15: aload_1         // 加载局部变量表中的lock引用
      16: monitorexit     // 正常退出同步块,释放锁
      17: goto            25
      20: astore_2        // 异常处理:存储异常对象
      21: aload_1         // 加载lock引用
      22: monitorexit     // 异常情况下释放锁
      23: aload_2         // 抛出异常
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any  // 捕获7-17行的所有异常

关键指令解析

  • monitorenter
    

    :尝试获取锁对象关联的 Monitor 所有权

    • 若 Monitor 的计数器为 0,当前线程获得所有权,计数器变为 1
    • 若当前线程已持有该 Monitor,计数器加 1(体现重入性)
    • 若其他线程持有该 Monitor,当前线程进入阻塞状态
  • monitorexit
    

    :释放 Monitor 所有权

    • 计数器减 1,当计数器为 0 时,彻底释放 Monitor,唤醒等待线程
    • 出现两次是为了保证异常情况下锁也能被释放

2.2 同步方法的字节码解析

同步方法不依赖monitorenter/monitorexit指令,而是通过方法常量池中的ACC_SYNCHRONIZED标志实现:

public class SyncMethodDemo {
    public synchronized void syncMethod() {
        System.out.println("同步方法");
    }
}

字节码中的方法标志:

public synchronized void syncMethod();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 同步方法标志
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic       #2  // 访问System.out
       3: ldc             #3  // 加载字符串"同步方法"
       5: invokevirtual   #4  // 调用println方法
       8: return

执行逻辑

  • 当 JVM 调用带有

    ACC_SYNCHRONIZED
    

    标志的方法时,会先尝试获取锁

    • 实例方法:锁对象为this
    • 静态方法:锁对象为当前类的Class对象
  • 方法执行完毕后(无论正常返回还是抛出异常),自动释放锁

三、底层核心:对象头与 Monitor 机制

synchronized的实现依赖于 Java 对象头和 Monitor(监视器锁),这是理解其底层原理的关键。

3.1 Java 对象头结构

在 JVM 中,每个对象在内存中的布局分为三部分:

  • 对象头(Object Header):存储对象的元数据信息,包括锁状态
  • 实例数据(Instance Data):存储对象的属性值
  • 对齐填充(Padding):确保对象大小为 8 字节的整数倍

其中,对象头是实现synchronized的核心,由两部分组成(以 32 位 JVM 为例):

组成部分长度说明
Mark Word32 位存储锁状态、哈希码等信息
Class Metadata Pointer32 位指向类元数据的指针

Mark Word 的动态变化
Mark Word 会根据对象的锁状态动态改变存储内容,32 位 JVM 中的结构如下:

锁状态存储内容(32 位)锁标志位
无锁状态哈希码(25 位) + 分代年龄(4 位) + 偏向锁标志(1 位) + 锁标志位(2 位)01
偏向锁线程 ID(23 位) + Epoch(2 位) + 分代年龄(4 位) + 偏向锁标志(1 位) + 锁标志位(2 位)01
轻量级锁指向栈中锁记录的指针(30 位) + 锁标志位(2 位)00
重量级锁指向 Monitor 的指针(30 位) + 锁标志位(2 位)10
GC 标记空 + 锁标志位(2 位)11

注:偏向锁标志为 1 时表示可偏向,01 的锁标志位在无锁和偏向锁状态下共用,通过偏向锁标志区分。

3.2 Monitor(监视器锁)的实现

synchronized的排他性同步基于 Monitor 实现,在 HotSpot 虚拟机中,Monitor 由 C++ 的ObjectMonitor类实现,核心结构如下:

struct ObjectMonitor {
    _header       : 指向对象头的Mark Word
    _count        : 记录线程获取锁的次数(重入计数器)
    _waiters      : 调用wait()方法的线程组成的等待队列
    _owner        : 指向当前持有锁的线程
    _EntryList    : 等待获取锁的线程组成的阻塞队列
    _recursions   : 重入次数(与_count类似,用于递归场景)
    ...
};

Monitor 的工作流程

  1. 线程进入同步块时,首先进入_EntryList队列等待

  2. 当 Monitor 的_owner为 null 时,线程从_EntryList中移出,将_owner指向当前线程,_count加 1

  3. 若当前线程已持有该 Monitor(重入),只需将_count加 1

  4. 线程调用

    wait()
    

    方法时:

    • _count减 1,若_count变为 0,_owner置为 null
    • 线程进入_waiters队列等待
  5. 线程调用

    notify()
    

    方法时:

    • _waiters队列中随机唤醒一个线程,移至_EntryList
  6. 线程退出同步块时:

    • _count减 1,若_count变为 0,_owner置为 null
    • 唤醒_EntryList中的线程竞争锁

四、锁升级机制深度解析

JDK 1.6 引入的锁升级机制是synchronized性能提升的关键,通过自适应锁状态,减少不必要的性能开销。锁升级是一个单向过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

4.1 偏向锁(Biased Locking)

设计初衷:在无多线程竞争的场景下,消除锁获取的原子操作开销。

实现原理
当第一个线程访问同步块时,JVM 通过 CAS 操作将线程 ID 写入对象头的 Mark Word,此时对象进入偏向锁状态。后续该线程再次进入同步块时,只需比对 Mark Word 中的线程 ID,无需再次执行 CAS 操作,几乎无性能开销。

偏向锁的撤销
当其他线程尝试获取锁时,会触发偏向锁的撤销,步骤如下:

  1. 暂停持有偏向锁的线程
  2. 检查持有线程的状态:
    • 若线程已退出同步块,将对象恢复为无锁状态
    • 若线程仍在同步块中,升级为轻量级锁
  3. 唤醒被暂停的线程

代码示例

public class BiasedLockDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        // 线程1获取偏向锁
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程1获取偏向锁");
                try {
                    // 保持锁持有状态,方便观察
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Thread-1").start();
        
        // 等待线程1获取偏向锁
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 线程2尝试获取锁,触发偏向锁撤销
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2获取锁(偏向锁已撤销)");
            }
        }, "Thread-2").start();
    }
}

适用场景:单线程重复访问同步块(如单线程操作集合、单线程初始化资源)。

4.2 轻量级锁(Lightweight Locking)

当存在轻微线程竞争时,偏向锁会升级为轻量级锁,基于 CAS 操作实现非阻塞同步。

加锁流程

  1. 线程在栈帧中创建锁记录(Lock Record),存储对象 Mark Word 的副本(Displaced Mark Word)
  2. 通过 CAS 操作尝试将对象的 Mark Word 替换为指向锁记录的指针:
    • 成功:线程获取轻量级锁
    • 失败:说明存在竞争,进入自旋重试

自旋优化
竞争线程不会立即阻塞,而是通过循环重试 CAS 操作(自旋),避免线程切换的开销。自旋次数由 JVM 自适应调整(默认最大 10 次)。

解锁流程

  1. 通过 CAS 操作将 Displaced Mark Word 写回对象头:
    • 成功:无竞争,解锁完成
    • 失败:存在竞争,轻量级锁膨胀为重量级锁

代码示例

public class LightweightLockDemo {
    private static final Object lock = new Object();
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        // 两个线程交替获取锁,触发轻量级锁
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果:" + count); // 预期200000
    }
}

适用场景:多线程交替执行同步块,竞争不激烈的场景。

4.3 重量级锁(Heavyweight Locking)

竞争激烈或自旋失败时,轻量级锁会膨胀为重量级锁,此时依赖操作系统的互斥锁(Mutex)实现同步。

实现原理
重量级锁的实现依赖操作系统的内核态互斥锁,线程竞争失败时会被操作系统放入内核等待队列,锁释放时由操作系统唤醒等待线程。

性能特点

  • 线程阻塞 / 唤醒需要从用户态切换到内核态,开销较大
  • 适合长时间持有锁的场景(如复杂计算、IO 操作)

与操作系统的交互

  • 重量级锁基于操作系统的pthread_mutex_t(互斥锁)实现
  • 线程状态切换(就绪→阻塞→就绪)需要内核介入
  • 上下文切换成本高,约为用户态操作的 100 倍以上

代码示例

public class HeavyweightLockDemo {
    private static final Object lock = new Object();
    
    public static void main(String[] args) {
        // 多个线程同时竞争锁,触发重量级锁
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (lock) {
                    try {
                        // 长时间持有锁,模拟复杂操作
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "执行完成");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "Thread-" + i).start();
        }
    }
}

适用场景:多线程激烈竞争,或锁持有时间较长的场景。

五、性能优化实战与最佳实践

5.1 控制同步粒度

反例:过大的同步范围导致性能损耗

// 不推荐:同步整个方法
public synchronized void processData() {
    // 非临界区操作(日志、参数校验等)
    log.info("开始处理数据");
    validateParams();
    
    // 临界区操作(仅需同步此处)
    sharedResource.update();
    
    // 非临界区操作
    log.info("数据处理完成");
}

优化:仅同步必要的临界区

// 推荐:缩小同步范围
public void processData() {
    // 非临界区操作(无需同步)
    log.info("开始处理数据");
    validateParams();
    
    // 仅同步临界区
    synchronized (lock) {
        sharedResource.update();
    }
    
    // 非临界区操作(无需同步)
    log.info("数据处理完成");
}

5.2 锁分离策略

通过拆分锁对象,降低锁竞争强度:

// 订单处理服务,按用户ID分片
public class OrderService {
    // 分片锁数组
    private final Object[] locks = new Object[16];
    private final Map<Long, Order> orderMap = new ConcurrentHashMap<>();
    
    public OrderService() {
        // 初始化锁对象
        for (int i = 0; i < locks.length; i++) {
            locks[i] = new Object();
        }
    }
    
    // 根据用户ID路由到不同的锁
    public void updateOrder(Long userId, Order order) {
        // 计算分片索引
        int lockIndex = (int) (userId % locks.length);
        synchronized (locks[lockIndex]) {
            orderMap.put(userId, order);
            // 其他订单处理逻辑
        }
    }
    
    public Order getOrder(Long userId) {
        int lockIndex = (int) (userId % locks.length);
        synchronized (locks[lockIndex]) {
            return orderMap.get(userId);
        }
    }
}

优势:不同用户的请求会竞争不同的锁,大幅降低锁冲突概率。

5.3 合理使用偏向锁

JVM 参数控制偏向锁行为:

# 启用偏向锁(默认启用,JDK 6+)
-XX:+UseBiasedLocking

# 禁用偏向锁
-XX:-UseBiasedLocking

# 偏向锁延迟生效时间(默认4秒,0表示立即生效)
-XX:BiasedLockingStartupDelay=0

性能测试:单线程场景下偏向锁的优势

public class BiasedLockBenchmark {
    private static final Object lock = new Object();
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        // 等待偏向锁生效(默认延迟4秒)
        Thread.sleep(5000);
        
        long start = System.nanoTime();
        // 单线程重复获取锁
        for (int i = 0; i < 10_000_000; i++) {
            synchronized (lock) {
                count++;
            }
        }
        long end = System.nanoTime();
        
        System.out.println("耗时:" + (end - start) / 1_000_000 + "ms");
    }
}

测试结果

  • 启用偏向锁:约 50ms
  • 禁用偏向锁(轻量级锁):约 300ms
  • 直接使用重量级锁:约 1000ms

六、常见问题与解决方案

6.1 锁对象变更导致同步失效

问题:锁对象被修改后,不同线程会持有不同的锁,导致同步失效。

// 错误示例
public class BadLockExample {
    private Object lock = new Object();
    
    public void doSomething() {
        synchronized (lock) {
            // 错误:修改锁对象
            lock = new Object();
            // 临界区操作
        }
    }
}

解决方案:使用final修饰锁对象,确保其不可变。

// 正确示例
public class GoodLockExample {
    // final保证锁对象不会被修改
    private final Object lock = new Object();
    
    public void doSomething() {
        synchronized (lock) {
            // 临界区操作
        }
    }
}

6.2 重入锁导致的死锁误解

问题:误认为同一线程嵌套获取锁会导致死锁。

解析synchronized是可重入锁,通过 Monitor 的_count计数器实现重入,同一线程可以多次获取同一把锁。

public class ReentrantLockExample {
    public synchronized void outerMethod() {
        System.out.println("进入外层方法");
        innerMethod(); // 同一线程可重入
    }
    
    public synchronized void innerMethod() {
        System.out.println("进入内层方法");
    }
    
    public static void main(String[] args) {
        new ReentrantLockExample().outerMethod();
    }
}

输出结果

进入外层方法
进入内层方法

6.3 wait/notify 使用不当导致的问题

问题notify()唤醒的是任意等待线程,无法保证唤醒特定线程;未在循环中检查条件导致虚假唤醒。

解决方案

  • 使用notifyAll()唤醒所有等待线程(或使用Condition实现精准唤醒)
  • 在循环中检查等待条件,避免虚假唤醒
public class WaitNotifyExample {
    private final Object lock = new Object();
    private boolean condition = false;
    
    public void waitForCondition() throws InterruptedException {
        synchronized (lock) {
            // 在循环中检查条件,避免虚假唤醒
            while (!condition) {
                lock.wait(); // 释放锁并等待
            }
            // 处理逻辑
        }
    }
    
    public void setCondition() {
        synchronized (lock) {
            condition = true;
            lock.notifyAll(); // 唤醒所有等待线程
        }
    }
}

七、总结

synchronized的底层实现是 Java 并发机制的核心知识点,其演进反映了 JVM 对性能优化的持续追求:

  1. 实现基础:基于对象头的 Mark Word 存储锁状态,通过 Monitor 实现排他性同步
  2. 锁升级路径:无锁→偏向锁→轻量级锁→重量级锁,自适应不同竞争强度
  3. 性能优化:通过控制同步粒度、使用锁分离策略,充分利用锁升级机制提升性能
  4. 最佳实践:使用private final修饰锁对象,避免锁对象变更;根据竞争强度选择合适的同步策略

掌握synchronized的底层原理,不仅能帮助我们写出更高效的并发代码,更能深入理解 JVM 的并发设计思想,为应对复杂并发场景打下坚实基础。在实际开发中,需结合业务特点选择合适的同步策略,在线程安全与性能之间找到最佳平衡点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值