1. synchronized的基本概念

synchronized可以用于修饰方法或代码块:

  • 同步方法:将整个方法声明为同步方法,使得同一时间只有一个线程能够执行这个方法。
  • 同步代码块:对某一段代码进行同步,使得同一时间只有一个线程能够执行这段代码。

synchronized的使用确保了线程在访问被保护的代码块时,会自动获取该对象的监视器锁(Monitor),并在退出时释放锁。

2. synchronized的底层原理

synchronized的底层实现依赖于Java对象头和Monitor锁机制。Java虚拟机(JVM)中的Monitor锁主要依赖操作系统的底层同步机制,如互斥锁(Mutex),来实现线程同步。

2.1. Java对象头(Object Header)

Java中的每个对象在内存中都有一个对象头(Object Header),它包含了与对象有关的元数据。其中,最重要的部分之一就是用于同步的Mark Word。

  • Mark Word:在不同的情况下,Mark Word的内容会有所不同。在无锁状态下,Mark Word中包含对象的哈希码、分代年龄等信息;在加锁状态下,Mark Word中包含指向Monitor对象的指针。

Mark Word是synchronized底层实现的核心部分,它在对象锁的获取和释放过程中起到关键作用。

2.2. Monitor锁

Monitor锁是synchronized实现同步的核心机制,每个对象都有一个关联的Monitor对象。当线程尝试进入synchronized代码块时,它必须先获取与该对象关联的Monitor锁。

Monitor锁在Java中通过操作系统的底层机制来实现,例如POSIX线程中的互斥锁(Mutex),这使得synchronized的实现具有较高的安全性和可靠性。

Monitor主要包含以下几个关键部分:

  • Owner:当前持有Monitor锁的线程。
  • EntryList:所有等待获取Monitor锁的线程会被放置在这个队列中。
  • WaitSet:调用Object.wait()方法后进入等待状态的线程会被放置在这个队列中。

Monitor的工作机制如下:

  1. 获取锁:当一个线程尝试进入synchronized代码块时,它会首先检查该对象的Monitor锁是否已被其他线程持有。如果没有其他线程持有锁,线程将成功获取锁并将自己设置为Owner。

  2. 阻塞线程:如果Monitor锁已经被其他线程持有,当前线程将被阻塞,并被放入EntryList中等待。

  3. 释放锁:当线程退出synchronized代码块时,它将释放Monitor锁。如果有其他线程在等待这个锁,系统将从EntryList中选择一个线程并授予它锁的所有权。

2.3. JVM指令级实现

在JVM中,synchronized关键字的底层实现依赖于两个字节码指令:

  • monitorenter:当线程进入synchronized代码块时,会执行monitorenter指令,它用于尝试获取Monitor锁。
  • monitorexit:当线程退出synchronized代码块时,会执行monitorexit指令,它用于释放Monitor锁。

例子:

public class SyncExample {
    public void syncMethod() {
        synchronized (this) {
            // critical section
        }
    }
}

在上述代码中,编译器会生成以下字节码:

0: aload_0
1: dup
2: monitorenter
3: // critical section
4: monitorexit
5: return

可以看到,编译器在synchronized代码块的前后分别插入了monitorentermonitorexit指令。

3. synchronized的优化——锁的膨胀与偏向锁

为了提高synchronized的性能,Java虚拟机对锁的实现进行了多种优化,主要包括偏向锁轻量级锁重量级锁的升级机制。

3.1. 偏向锁(Biased Locking)

偏向锁是为了在无竞争的情况下减少加锁和解锁操作的开销而引入的。它的基本思想是,如果一个线程获取了锁,那么锁就会进入偏向模式,此时Mark Word会记录该线程的ID。如果该线程再次尝试获取锁,不需要再进行加锁操作,直接进入同步代码块即可。

当另一个线程尝试获取该锁时,偏向锁会被撤销,然后进入轻量级锁或重量级锁的状态。

3.2. 轻量级锁(Lightweight Locking)

轻量级锁是在无线程竞争的情况下,通过CAS操作(Compare And Swap)来减少传统的互斥锁(Mutex)的使用,以此来提高性能。当锁是轻量级锁时,Mark Word中存储的是指向栈中锁记录的指针。

在没有竞争的情况下,轻量级锁的开销非常小,但一旦出现竞争,轻量级锁会膨胀为重量级锁。

3.3. 重量级锁(Heavyweight Locking)

重量级锁是传统的Monitor锁。当出现线程竞争时,轻量级锁会膨胀为重量级锁,此时线程会被阻塞,直到持有锁的线程释放锁。重量级锁通过操作系统的互斥锁(Mutex)实现,性能较低,但能够确保线程安全。

3.4. 锁的状态转换

锁的状态转换过程如下:

  1. 偏向锁:当一个线程首次获取锁时,锁进入偏向状态,Mark Word中记录线程ID。
  2. 轻量级锁:当另一个线程尝试获取已经偏向的锁时,偏向锁撤销,升级为轻量级锁,通过CAS操作尝试获取锁。
  3. 重量级锁:当轻量级锁竞争失败(即CAS操作失败)时,锁会膨胀为重量级锁,此时线程会被阻塞,等待锁的释放。

4. synchronized的性能与使用建议

synchronized关键字在早期的Java版本中被认为性能较低,因为它依赖于重量级的Monitor锁。然而,在JDK 1.6之后,通过引入偏向锁和轻量级锁机制,synchronized的性能得到了显著提升。

4.1. 使用建议
  • 合理选择同步范围:尽量缩小synchronized代码块的范围,减少锁的竞争和阻塞。

  • 避免锁的嵌套:嵌套的锁容易导致死锁问题,应尽量避免。如果必须使用,确保锁的获取顺序一致。

  • 利用高层次并发工具:在可能的情况下,使用java.util.concurrent包中的高级并发工具(如ReentrantLockSemaphore等),它们提供了更灵活和高效的线程同步机制。

  • 选择合适的锁类型:根据场景选择适合的锁类型,例如在多读少写的场景中,ReentrantReadWriteLock可能比synchronized更加高效。

5. 总结

synchronized关键字是Java中提供的一种简单且强大的线程同步机制,其底层实现依赖于Monitor锁和操作系统的同步原语。通过在对象的Mark Word中记录锁的信息,synchronized能够在保证线程安全的同时,结合偏向锁、轻量级锁和重量级锁等优化策略,尽可能地提高性能。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐