面试八股之从jvm层面深入解析Java中的synchronized关键字

发布于:2025-08-13 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、synchronized概述

synchronized是Java中最基本的同步机制,用于控制多个线程对共享资源的访问,确保同一时刻只有一个线程可以执行特定代码段或访问特定对象。它是Java内置的互斥锁实现,能够有效解决多线程环境下的原子性、可见性和有序性问题。

基本作用

  1. 原子性:确保互斥操作,防止多个线程同时执行临界区代码
  2. 可见性:保证锁释放前对共享变量的修改对其他线程可见
  3. 有序性:防止指令重排序,确保代码执行顺序符合预期

二、synchronized的三种使用方式

1. 同步实例方法

public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
}
  • 锁对象:当前实例对象(this)
  • 作用范围:整个方法体

2. 同步静态方法

public class StaticCounter {
    private static int count = 0;
    
    public static synchronized void increment() {
        count++;
    }
}
  • 锁对象:当前类的Class对象(StaticCounter.class)
  • 作用范围:整个静态方法体

3. 同步代码块

public class BlockCounter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized(lock) {
            count++;
        }
    }
}
  • 锁对象:可以是任意对象实例
  • 作用范围:代码块内部
  • 灵活性高,可以精确控制同步范围

三、JVM层面的实现原理

1. 对象头与Mark Word

在HotSpot虚拟机中,Java对象在内存中的布局分为三部分:

  1. 对象头(Header)
  2. 实例数据(Instance Data)
  3. 对齐填充(Padding)

其中对象头包含两部分:

  • Mark Word:存储对象的hashCode、GC分代年龄、锁状态等信息
  • 类型指针:指向类元数据的指针

在32位JVM中,Mark Word结构如下:

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

2. 锁升级过程

JDK1.6之后,synchronized进行了重要优化,引入了锁升级机制,而不是直接使用重量级锁。锁的状态会随着竞争情况从低到高逐步升级:

  1. 无锁状态:新创建的对象处于无锁状态
  2. 偏向锁:适用于只有一个线程访问同步块的场景
  3. 轻量级锁:当有少量线程竞争时,通过CAS操作获取锁
  4. 重量级锁:当竞争激烈时,升级为操作系统层面的互斥量
偏向锁(Biased Locking)
  • 目的:减少无竞争情况下的同步开销
  • 原理:在Mark Word中记录偏向线程ID
  • 优点:加锁解锁不需要额外操作
  • 适用场景:单线程访问同步块
轻量级锁(Lightweight Locking)
  • 目的:减少多线程交替执行同步块时的性能消耗
  • 原理:使用CAS操作将Mark Word替换为指向线程栈中锁记录的指针
  • 优点:避免线程阻塞
  • 缺点:自旋会消耗CPU
重量级锁(Heavyweight Locking)
  • 目的:处理高竞争情况
  • 原理:通过操作系统的互斥量(mutex)实现
  • 特点:线程会阻塞,性能开销大

3. 字节码层面分析

编译后的同步代码块会在字节码中使用monitorentermonitorexit指令实现:

public void syncMethod();
  Code:
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter          // 进入同步块
     4: aload_1
     5: monitorexit           // 正常退出同步块
     6: goto          14
     9: astore_2
    10: aload_1
    11: monitorexit           // 异常退出同步块
    12: aload_2
    13: athrow
    14: return

可以看到编译器会自动生成异常处理逻辑,确保锁在异常情况下也能被释放。

四、锁优化技术

1. 自旋锁与自适应自旋

  • 自旋锁:线程不立即阻塞,而是执行忙循环(自旋)等待锁释放
  • 自适应自旋:JVM根据之前自旋等待的成功率动态调整自旋时间

2. 锁消除(Lock Elimination)

JIT编译器通过逃逸分析,发现某些锁对象不可能被共享时,会消除这些锁操作。

public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
}

在这个例子中,StringBuffer是局部变量,不会被其他线程访问,JVM会消除其内部同步操作。

3. 锁粗化(Lock Coarsening)

将多个连续的锁操作合并为一个更大的锁操作,减少频繁同步带来的性能损耗。

public void method() {
    synchronized(lock) {
        // 操作1
    }
    synchronized(lock) {
        // 操作2
    }
    // 可能被优化为
    synchronized(lock) {
        // 操作1
        // 操作2
    }
}

五、性能考量与最佳实践

1. 性能比较

  • 无竞争:偏向锁 > 轻量级锁 > 重量级锁
  • 低竞争:轻量级锁 > 偏向锁 > 重量级锁
  • 高竞争:重量级锁更合适

2. 使用建议

  1. 减小同步范围:只在必要的地方加锁
  2. 降低锁粒度:使用多个锁控制不同资源
  3. 避免锁嵌套:容易导致死锁
  4. 考虑替代方案:在适当场景使用java.util.concurrent包中的并发工具

3. 示例:双重检查锁定(Double-Checked Locking)

public class Singleton {
    private volatile static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

注意:必须使用volatile关键字防止指令重排序问题。

六、总结

synchronized关键字是Java并发编程的基础构建块,从JDK1.0开始就存在,经过多次优化(尤其是JDK1.6的锁升级机制)后,性能已经大幅提升。理解其JVM层面的实现原理,有助于我们编写更高效、更安全的并发程序。

在实际开发中,应根据具体场景选择合适的同步策略,对于简单同步需求,synchronized仍然是一个简单有效的选择;对于更复杂的并发场景,可以考虑java.util.concurrent包中更高级的并发工具。


网站公告

今日签到

点亮在社区的每一天
去签到