好的,让我们深入探讨Java字节码指令 monitorenter 和 monitorexit,以及它们在 synchronized 锁底层实现中的作用。
讲座:Java synchronized 的字节码实现原理
引言:并发控制的基石
在多线程编程中,并发控制是至关重要的。Java 提供了 synchronized 关键字作为一种内置的锁机制,用于保证在同一时刻只有一个线程可以访问特定的代码块或方法。但 synchronized 的底层实现是什么呢?答案就隐藏在 Java 字节码指令 monitorenter 和 monitorexit 中。
一、synchronized 的基本用法
首先,我们回顾一下 synchronized 的两种主要用法:
- 
同步代码块: public class SynchronizedBlockExample { private Object lock = new Object(); public void doSomething() { synchronized (lock) { // 需要同步的代码 System.out.println(Thread.currentThread().getName() + ": Doing something synchronized."); try { Thread.sleep(1000); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } } }
- 
同步方法: public class SynchronizedMethodExample { public synchronized void doSomething() { // 需要同步的代码 System.out.println(Thread.currentThread().getName() + ": Doing something synchronized."); try { Thread.sleep(1000); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } } }
在同步代码块中,我们需要显式指定一个对象作为锁。在同步方法中,锁是隐式的:对于实例方法,锁是 this 对象;对于静态方法,锁是该类的 Class 对象。
二、字节码分析:monitorenter 和 monitorexit 的登场
为了理解 synchronized 的底层实现,我们需要查看编译后的字节码。我们可以使用 javap -c 命令来反编译上面的代码。
- 
同步代码块的字节码: public void doSomething(); Code: 0: aload_0 1: getfield #2 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter 7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 10: new #4 // class java/lang/StringBuilder 13: dup 14: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 17: invokestatic #6 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 20: invokevirtual #7 // Method java/lang/Thread.getName:()Ljava/lang/String; 23: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 26: ldc #9 // String : Doing something synchronized. 28: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 37: ldc2_w #12 // long 1000l 40: invokestatic #14 // Method java/lang/Thread.sleep:(J)V 43: goto 51 46: astore_2 47: aload_2 48: invokevirtual #15 // Method java/lang/Throwable.printStackTrace:()V 51: aload_1 52: monitorexit 53: goto 61 56: astore_3 57: aload_1 58: monitorexit 59: aload_3 60: athrow 61: return Exception table: from to target type 7 43 46 Class java/lang/InterruptedException 7 53 56 any 56 59 56 any
- 
同步方法的字节码: public synchronized void doSomething(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #4 // class java/lang/StringBuilder 6: dup 7: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 10: invokestatic #6 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 13: invokevirtual #7 // Method java/lang/Thread.getName:()Ljava/lang/String; 16: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: ldc #9 // String : Doing something synchronized. 21: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 30: ldc2_w #12 // long 1000l 33: invokestatic #14 // Method java/lang/Thread.sleep:(J)V 36: goto 44 39: astore_1 40: aload_1 41: invokevirtual #15 // Method java/lang/Throwable.printStackTrace:()V 44: return Exception table: from to target type 0 36 39 Class java/lang/InterruptedException
我们可以看到,同步代码块的关键在于 monitorenter 和 monitorexit 指令。同步方法没有显式的 monitorenter 和 monitorexit 指令,但它的方法标志位 ACC_SYNCHRONIZED 会指示 JVM 在方法调用前后自动进行加锁和解锁操作。
深入解析 monitorenter 和 monitorexit
- 
monitorenter: - 尝试获取指定对象的锁(monitor)。
- 如果对象的 monitor 的进入数为 0,则该线程可以进入,并将进入数设置为 1,此时该线程为 monitor 的所有者。
- 如果线程已经拥有该 monitor 的所有权,则进入数加 1。
- 如果其他线程拥有该 monitor 的所有权,则该线程被阻塞,直到 monitor 的进入数为 0,才能再次尝试获取。
 
- 
monitorexit: - 释放指定对象的锁(monitor)。
- 将 monitor 的进入数减 1。
- 如果减 1 后,进入数为 0,则该线程释放 monitor 的所有权,允许其他线程尝试获取 monitor。
 
关键点:
- monitorenter和- monitorexit必须配对使用,确保锁的正确释放。
- JVM 保证 monitorenter和monitorexit的配对出现,即使在发生异常的情况下也能确保锁被释放。这通过Exception table实现,在任何情况下,包括异常,都会执行monitorexit。
- 每个对象都有一个 monitor 与之关联。
三、Monitor 的内部结构
Monitor 可以认为是一种同步工具,也可以描述为一种数据结构,它被设计成允许并发线程互斥地访问共享资源。 HotSpot虚拟机中,Monitor是由ObjectMonitor实现的。ObjectMonitor类中维护了几个关键字段:
| 字段 | 描述 | 
|---|---|
| _owner | 指向持有该monitor的线程。当 _owner为null时,表示该monitor当前未被任何线程持有,任何线程都有机会竞争获取该monitor;当_owner指向一个具体的线程时,则表示该monitor已经被此线程独占。 | 
| _EntryList | 是一个等待队列,用于存放那些尝试获取monitor但未能成功的线程。这些线程会被封装成ObjectWaiter对象,并加入到 _EntryList中。当_owner线程释放monitor后,_EntryList中的线程有机会再次竞争获取monitor。JVM通常会按照一定的策略(如FIFO)从_EntryList中选择一个线程来唤醒,使其尝试获取monitor。 | 
| _WaitSet | 是另一个等待队列,与 _EntryList不同的是,进入_WaitSet的线程是因为调用了wait()方法而主动释放了monitor,并进入等待状态。这些线程只有在被其他线程调用notify()或notifyAll()方法唤醒后,才会重新进入_EntryList,再次竞争获取monitor。因此,_WaitSet中的线程处于更深层次的等待状态,它们不仅需要等待monitor可用,还需要等待特定的条件被满足。 | 
| _count | 记录monitor被持有的次数。当线程成功获取monitor时, _count会递增;当线程释放monitor时,_count会递减。_count的主要作用是支持可重入锁的特性。当一个线程已经持有monitor时,它可以再次获取同一个monitor而不会被阻塞,每次获取_count都会递增。只有当线程完全释放monitor,即_count递减至0时,其他线程才有机会获取该monitor。 | 
| _recursions | 记录重入的次数。当一个线程首次获取锁时, _recursions被设置为0。如果同一个线程再次获取该锁(重入),则_recursions递增。释放锁时,只有当_recursions减为0,且_count也减为0时,锁才会被完全释放,其他线程才能获取该锁。 | 
四、synchronized 的实现细节
- 
锁的获取: 当线程执行到 monitorenter指令时,会尝试获取对象的 monitor。如果 monitor 的_owner字段为 null,表示该 monitor 未被任何线程持有,则该线程成功获取锁,并将_owner设置为当前线程,_count加 1。如果 _owner字段指向当前线程,表示该线程已经持有锁,则进行重入,_count加 1。如果 _owner字段指向其他线程,表示该 monitor 已经被其他线程持有,则当前线程进入_EntryList队列等待。
- 
锁的释放: 当线程执行到 monitorexit指令时,会释放对象的 monitor。将_count减 1。如果 _count减为 0,表示该线程完全释放了锁,将_owner设置为 null,并唤醒_EntryList队列中的一个线程,使其尝试获取锁。
- 
异常处理: 为了保证锁的正确释放,即使在发生异常的情况下,JVM 也会确保执行 monitorexit指令。这通过Exception table实现,在任何情况下,包括异常,都会执行monitorexit。
五、锁的优化:从重量级锁到轻量级锁、偏向锁
早期的 synchronized 实现是基于操作系统的互斥量(Mutex),这是一种重量级锁。重量级锁的获取和释放涉及到用户态和内核态的切换,开销较大。
为了减少锁的开销,Java 虚拟机 (JVM) 引入了锁的优化机制,包括:
- 
偏向锁(Biased Locking): 偏向锁的思想是,如果一个锁总是被同一个线程持有,则该线程获取锁时不需要进行任何同步操作,直接进入同步代码块。只有当有其他线程尝试获取锁时,偏向锁才会升级为轻量级锁。 偏向锁通过在对象头中记录偏向线程 ID 来实现。当线程第一次获取锁时,JVM 会将对象头的 Mark Word 设置为偏向模式,并将线程 ID 记录在对象头中。以后该线程再次获取锁时,只需要检查对象头的 Mark Word 是否为偏向模式,以及偏向线程 ID 是否为当前线程 ID,如果都满足条件,则直接进入同步代码块,不需要进行任何同步操作。 
- 
轻量级锁(Lightweight Locking): 轻量级锁的思想是,在没有多线程竞争的情况下,避免使用重量级锁,而是使用 CAS(Compare and Swap)操作来尝试获取锁。 轻量级锁通过在线程栈帧中创建一个锁记录(Lock Record)来实现。当线程进入同步代码块时,JVM 会将对象头的 Mark Word 复制到锁记录中,然后尝试使用 CAS 操作将对象头的 Mark Word 更新为指向锁记录的指针。如果 CAS 操作成功,表示线程成功获取锁,可以进入同步代码块。如果 CAS 操作失败,表示有其他线程正在竞争锁,此时轻量级锁会膨胀为重量级锁。 
- 
自旋锁(Spin Locking): 自旋锁的思想是,当线程尝试获取锁失败时,不会立即阻塞,而是循环尝试获取锁,直到获取成功或者达到一定的自旋次数。 自旋锁可以减少线程切换的开销,但如果锁被其他线程长时间持有,自旋锁会消耗大量的 CPU 资源。 
锁的升级过程如下:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
表格:锁的比较
| 特性 | 偏向锁 | 轻量级锁 | 重量级锁 | 
|---|---|---|---|
| 适用场景 | 单线程访问同步代码块 | 多线程交替访问同步代码块,竞争不激烈 | 多线程并发访问同步代码块,竞争激烈 | 
| 实现方式 | 对象头记录偏向线程 ID | CAS 操作更新对象头 Mark Word | 操作系统互斥量(Mutex) | 
| 开销 | 极低 | 较低 | 高 | 
| 优点 | 避免了不必要的同步操作,提高了性能 | 减少了线程切换的开销,适用于竞争不激烈的场景 | 保证了线程安全,适用于竞争激烈的场景 | 
| 缺点 | 如果有其他线程尝试获取锁,需要撤销偏向锁,开销较大 | 如果 CAS 操作失败,需要进行自旋或者膨胀为重量级锁,会消耗 CPU 资源 | 线程切换的开销较大,性能较低 | 
六、synchronized 的性能优化建议
- 
减少锁的持有时间: 只在必要的时候才进行同步,尽量减少同步代码块的大小。 
- 
使用读写锁(ReadWriteLock): 如果读操作远多于写操作,可以使用读写锁来提高并发性能。读写锁允许多个线程同时进行读操作,但只允许一个线程进行写操作。 
- 
使用并发容器(Concurrent Collections): Java 提供了许多并发容器,例如 ConcurrentHashMap、CopyOnWriteArrayList等,这些容器内部已经实现了线程安全,不需要显式地进行同步。
- 
避免死锁: 死锁是指多个线程互相等待对方释放锁,导致所有线程都无法继续执行的情况。为了避免死锁,可以遵循以下原则: - 避免持有多个锁。
- 按照固定的顺序获取锁。
- 使用定时锁(tryLock),在等待一定时间后放弃获取锁。
 
代码示例:使用读写锁
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    private String data;
    public String readData() {
        lock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + ": Reading data.");
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }
    public void writeData(String newData) {
        lock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + ": Writing data.");
            this.data = newData;
        } finally {
            lock.writeLock().unlock();
        }
    }
}七、总结:理解 synchronized 的底层运作
synchronized 关键字是 Java 中实现并发控制的重要手段。通过理解 monitorenter 和 monitorexit 字节码指令,以及 Monitor 的内部结构,我们可以更深入地了解 synchronized 的底层实现原理。JVM 针对 synchronized 锁的优化机制,如偏向锁、轻量级锁和自旋锁,可以有效地提高并发性能。在实际开发中,我们需要根据具体的应用场景选择合适的并发控制策略,以充分利用多核 CPU 的性能。
锁的优化减少了开销,合理的并发策略能够充分利用硬件资源。