synchronized 原理分析!

你好,我是猿java。

synchronized关键字是Java中用于实现线程同步的机制之一,它可以确保在同一时刻只有一个线程可以访问某个代码块或方法,从而避免线程之间的竞争条件和数据不一致的问题。这篇文章,我们将从字节码角度来剖析synchronized工作原理。

工作原理

synchronized 实现原理主要依赖于 Java对象的内置锁(也称为监视器锁,Monitor Lock)。

  1. 对象监视器(Monitor):

    • 每个Java对象都有一个与之关联的监视器(Monitor),这个监视器是实现同步的核心。
    • 当一个线程试图进入一个synchronized方法或代码块时,它必须首先获得对象的监视器锁。如果监视器锁已经被其他线程持有,那么当前线程将被阻塞,直到监视器锁被释放。
  2. 锁的获取和释放:

    • 对于实例方法,锁是对象实例的监视器。
    • 对于静态方法,锁是类对象的监视器。
    • 对于代码块,锁是指定对象的监视器。
    • 当线程进入synchronized方法或代码块时,它会尝试获取相应的监视器锁。如果锁不可用,它会进入阻塞状态,直到锁被释放。
    • 当线程退出synchronized方法或代码块时,它会释放监视器锁,从而允许其他阻塞的线程继续执行。
  3. 字节码级别的实现:

    • 在字节码级别,当编译器遇到synchronized关键字时,它会生成相应的监视器进入和退出指令。
    • 对于synchronized方法,编译器会在方法的开始和结束处插入monitorentermonitorexit指令。
    • 对于synchronized代码块,编译器会在代码块的开始和结束处插入monitorentermonitorexit指令。
  4. 偏向锁和轻量级锁:

    • 为了提高性能,Java引入了偏向锁和轻量级锁的机制。
    • 偏向锁:如果一个对象的锁被一个线程多次获取,那么该锁会偏向这个线程,从而减少获取锁的开销。
    • 轻量级锁:如果偏向锁被其他线程竞争,那么锁会升级为轻量级锁,通过自旋的方式尝试获取锁,而不是直接阻塞线程。
    • 如果竞争依然激烈,轻量级锁会升级为重量级锁,此时会导致线程阻塞。
  5. 锁的升级和降级:

    • 锁可以从偏向锁升级为轻量级锁,再升级为重量级锁。
    • 一旦锁升级为重量级锁,它不会降级为轻量级锁或偏向锁。

锁优化

在 Java中,为了提高多线程环境下的性能,Java虚拟机(JVM)对锁的实现进行了多种优化,包括偏向锁、轻量级锁、适应性自旋、锁消除和锁粗化。这些优化技术主要是为了减少锁的开销,提高并发性能。

偏向锁

偏向锁(Biased Locking)是为了减少同一线程多次获取锁的开销而设计的。

  • 原理:当一个线程首次获取锁时,锁会偏向这个线程,即在对象头中记录该线程ID。之后,如果该线程再次获取锁,不需要进行任何同步操作,只需检查对象头中的线程ID是否与当前线程匹配。
  • 撤销:如果有其他线程尝试获取偏向锁,则偏向锁会被撤销并升级为轻量级锁。

轻量级锁

轻量级锁(Lightweight Locking)是为了减少竞争不激烈的情况下的锁开销而设计的。

  • 原理:当线程尝试获取轻量级锁时,如果锁是空闲的,则通过CAS操作将锁对象的Mark Word复制到当前线程的栈帧中,并将Mark Word指向栈帧中的锁记录。如果获取成功,锁状态变为轻量级锁。
  • 自旋:如果锁已经被其他线程持有,当前线程会进行自旋,而不是直接进入阻塞状态。自旋的次数是有限的,如果超过一定次数,自旋失败,锁会升级为重量级锁。

适应性自旋

适应性自旋(Adaptive Spinning)是在轻量级锁的基础上进一步优化自旋等待的机制。

  • 原理:自旋的次数不再是固定的,而是根据前一次自旋的结果动态调整。如果前一次自旋成功,那么下一次自旋的次数会增加;如果前一次自旋失败,下一次自旋的次数会减少。
  • 优势:通过动态调整自旋次数,可以更好地适应不同的锁竞争情况,减少不必要的线程阻塞和上下文切换。

锁消除

锁消除(Lock Elimination)是编译器优化的一种技术,用于在编译期间消除不必要的锁操作。

  • 原理:在JIT编译过程中,编译器通过逃逸分析(Escape Analysis)确定某个对象是否只在单线程中使用。如果对象没有逃逸到其他线程,那么对该对象的锁操作是多余的,可以被消除。
  • 示例:在方法内部创建的局部变量对象,如果没有逃逸到方法外部或其他线程,则对该对象的同步操作可以被消除。

锁粗化

锁粗化(Lock Coarsening)是为了减少频繁获取和释放锁的开销而设计的。

  • 原理:如果编译器检测到在一段代码中频繁地对同一个对象进行加锁和解锁操作,它会将这些操作合并成一个更大的范围。在更大范围内进行一次加锁和解锁,从而减少锁的开销。
  • 示例:在循环中频繁进行加锁和解锁操作,可以将锁的范围扩大到整个循环外部。

通过上述锁优化的过程可以看出,锁优化技术的共同目标是提高多线程环境下的性能,减少锁的开销。具体来说:

  • 偏向锁:减少同一线程多次获取锁的开销。
  • 轻量级锁:减少竞争不激烈情况下的锁开销。
  • 适应性自旋:动态调整自旋次数,更好地适应不同的锁竞争情况。
  • 锁消除:在编译期间消除不必要的锁操作。
  • 锁粗化:减少频繁获取和释放锁的开销。

字节码分析

在Java中,synchronized关键字通过在字节码中插入特定的指令来实现线程同步。这些指令主要是 monitorentermonitorexit

让我们通过具体的例子和字节码分析来理解 synchronized 在不同类型上的实现。

同步实例方法

1
2
3
4
5
public class MyClass {
public synchronized void instanceMethod() {
System.out.println("Instance method");
}
}

编译后的字节码(可以使用 javap -c MyClass 命令查看):

1
2
3
4
5
6
7
8
9
public synchronized void instanceMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Instance method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

分析:

  • ACC_SYNCHRONIZED 标志在方法的访问标志中,表示这是一个同步方法。
  • JVM 会在调用这个方法时自动获取和释放对象实例的监视器锁(this对象)。

同步静态方法

1
2
3
4
5
public class MyClass {
public static synchronized void staticMethod() {
System.out.println("Static method");
}
}

编译后的字节码:

1
2
3
4
5
6
7
8
9
public static synchronized void staticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Static method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

分析:

  • ACC_SYNCHRONIZED 标志在方法的访问标志中,表示这是一个同步方法。
  • ACC_STATIC 标志表示这是一个静态方法。
  • JVM 会在调用这个方法时自动获取和释放类对象的监视器锁(MyClass.class)。

同步代码块

1
2
3
4
5
6
7
8
9
public class MyClass {
private final Object lock = new Object();

public void method() {
synchronized (lock) {
System.out.println("Synchronized block");
}
}
}

编译后的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
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: ldc #4 // String Synchronized block
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any

分析:

  • monitorenter 指令在进入同步块时执行,尝试获取lock对象的监视器锁。
  • monitorexit 指令在离开同步块时执行,释放lock对象的监视器锁。
  • 字节码中包括异常处理,以确保即使在异常情况下也能正确释放锁。

从上述字节码的分析可以看出:

  • 对于同步实例方法和静态方法,字节码中使用了ACC_SYNCHRONIZED标志,JVM会自动处理锁的获取和释放。
  • 对于同步代码块,字节码中显式地插入了monitorentermonitorexit指令,以手动处理锁的获取和释放。

总结

总结来说,synchronized关键字通过对象监视器锁的机制,确保在同一时刻只有一个线程能够执行synchronized方法或代码块,从而实现线程同步。Java通过偏向锁和轻量级锁等优化手段,提高了锁的性能,减少了线程阻塞的开销。

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing