Java Synchronized 关键字的使用方法及其字节码解析

Synchronized 是 Java 提供的线程同步关键字。线程同步机制是一套用于协调线程之间数据访问的机制,该机制通过临界区的排他性保障了线程安全。

1 Synchronized 使用方法

1.1 修饰代码块

1
2
3
4
5
6
7
Object sync = new Object();

void run() {
    synchronized(sync) { 
        //编写所有需要锁定的代码;
    }
}

修饰代码块时,监视器锁(monitor)是指定对象的实例。

1.2 修饰普通方法

1
2
3
public synchronized void run(){
    //...
}
1
2
3
4
public  void run(){
    synchronized(this);
    //...
}

修饰普通方法时,监视器锁(monitor)是指定对象的实例(this)。

1.3 修饰静态方法

1
2
// 锁定的是类对象
public synchronized static void func(){}

修饰静态静态方法,视器锁(monitor)是指定对象的 Class 实例(每个对象只有一个 Class 实例)。

注意
同一个类中的静态方法与非静态方法同时使用了 synchronized 后,它们之间是非互斥的。因为静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。

2 Synchronized 字节码分析

2.1 线程堆栈分析

我们编写以下测试用例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class SyncTest {
    private final static Object sync = new Object();

    public void accessResource() {
        synchronized (sync) {
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final SyncTest SyncTest = new SyncTest();
        for (int i = 0; i < 5; i++) {
            new Thread(SyncTest::accessResource).start();
        }
    }
}

上述代码中定义了一个 accessResource,并且使用同步代码块的方式对 accessResource 进行了同步,同时定义了五个线程调用 accessResource 方法,由于同步代码块的互斥性,只能有一个线程拿到 SyncTest monitor 锁,其他线程只能进入阻塞状态。

我们运行上述程序,同时打开 JConsole 工具,可以看到只有 Thread-0 成功锁定并进入 TIMED_WAITING 状态,其他线程都进入了 BLOCKED 状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
名称:Thread-0
状态:TIMED_WAITING
总阻止数:0, 总等待数:1

堆栈跟踪:
java.lang.Thread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:342)
java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
SyncTest.accessResource(SyncTest.java:9)
   - 已锁定 java.lang.Object@1c7e6cd
SyncTest$$Lambda$1/250421012.run(Unknown Source)
java.lang.Thread.run(Thread.java:750)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
名称:Thread-1
状态:java.lang.Object@1c7e6cd 上的 BLOCKED, 拥有者:Thread-0
总阻止数:1, 总等待数:0

堆栈跟踪:
SyncTest.accessResource(SyncTest.java:9)
SyncTest$$Lambda$1/250421012.run(Unknown Source)
java.lang.Thread.run(Thread.java:750)

// Thread-2 等状态与 Thread-1 相同,省略 ...

使用 jstack ${pid} 命令打印线程的堆栈信息,可以得到以下关键内容:

1
2
3
4
5
6
7
8
9
"Thread-0" #20 prio=5 os_prio=0 tid=0x00000161e10c3800 nid=0x14c4 waiting on condition [0x0000001544ffe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at java.lang.Thread.sleep(Thread.java:342)
        at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
        at SyncTest.accessResource(SyncTest.java:9)
        - locked <0x0000000715ccd240> (a java.lang.Object)
        at SyncTest$$Lambda$1/250421012.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)
  • Thread-0 持有 monitor<0x0000000715ccd240> 锁并且处于休眠状态中,所以其他线程将无法进入代码块。
1
2
3
4
5
6
"Thread-1" #21 prio=5 os_prio=0 tid=0x00000161e10c6000 nid=0x2a58 waiting for monitor entry [0x00000015450ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at SyncTest.accessResource(SyncTest.java:9)
        - waiting to lock <0x0000000715ccd240> (a java.lang.Object)
        at SyncTest$$Lambda$1/250421012.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)
  • Thread-1 线程进入 BLOCKED 状态并且在等待获取 monitor<0x0000000715ccd240> 锁,其他几个线程也是如此。

2.2 JVM 指令分析

使用 JDK 的 javap -v -l -c 命令对上述测试用例的 class 文件进行反编译,得到以下 JVM 指令:

 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class SyncTest {
  public SyncTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void accessResource();
    Code:
       0: getstatic     #2                  // 获取同步器 sync:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter                      // 执行 monitorenter JVM 指令,获取锁
       6: getstatic     #3                  // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #4                  // long 10l
      12: invokevirtual #6                  // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23                  // 跳转到 23: aload_1
      18: astore_2
      19: aload_2
      20: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1
      24: monitorexit                       // 正常退出时,释放锁
      25: goto          33
      28: astore_3
      29: aload_1
      30: monitorexit                       // 异常退出时,释放锁
      31: aload_3
      32: athrow
      33: return
    Exception table:
       from    to  target type
           6    15    18   Class java/lang/InterruptedException
           6    25    28   any
          28    31    28   any

  public static void main(java.lang.String[]);
    Code:
       0: new           #9                  // class SyncTest
       3: dup
       4: invokespecial #10                 // Method "<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: iconst_5
      12: if_icmpge     42
      15: new           #11                 // class java/lang/Thread
      18: dup
      19: aload_1
      20: dup
      21: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      24: pop
      25: invokedynamic #13,  0             // InvokeDynamic #0:run:(LSyncTest;)Ljava/lang/Runnable;
      30: invokespecial #14                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      33: invokevirtual #15                 // Method java/lang/Thread.start:()V
      36: iinc          2, 1
      39: goto          10
      42: return

  static {};
    Code:
       0: new           #16                 // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field sync:Ljava/lang/Object;
      10: return
}

根据上述内容可以得知,monitor 的 monitorenter 指令和 monitorexit 指令是成对出现的。一个 monitorenter 指令可以对应 1 ~ 2 个 monitorexit 指令,并且 monitorenter 指令一定在 monitorenter 指令之前出现。

注意

当同步代码中存在异常捕获相关的内容时,会发生一个 monitorenter 指令对应两个 monitorexit 指令的情况。这两个 monitorexit 指令分别用于处理代码的正常结束和代码出现异常的情况。

这样可以确保无论代码执行是否出现异常,都能正确释放对象的监视器锁,维护同步的正确性。

2.2.1 monitorenter

monitorenter 是 Java 字节码指令中的一条指令,用于获取对象的监视器锁 (monitor lock)。它与 monitorexit 指令成对出现,用于确保对对象的同步访问。

当线程执行到 monitorenter 指令时,它会尝试获取对象的监视器锁。如果该对象的监视器锁当前没有被其他线程持有,则当前线程成功获取该锁,并将计数器加一。此时,该线程成为了该对象的监视器锁的所有者。

如果在执行 monitorenter 指令之前,线程已经多次获取了该对象的监视器锁(即执行了多次 monitorenter 指令),那么每次执行 monitorenter 都会使计数器加一。这种情况下,每个 monitorenter 指令都必须与相应数量的 monitorexit 指令成对出现,以确保计数器的正确操作。

2.2.2 monitorexit

monitorexit 是 Java 字节码指令中的一条指令,用于释放对象的监视器锁 (monitor lock)。它与 monitorenter 指令成对出现,用于确保对对象的同步访问。

当线程执行到 monitorexit 指令时,它会释放当前线程持有的对象监视器锁,并且计数器减一。如果在执行 monitorexit 之前,线程多次获得了该对象的监视器锁(即执行了多次 monitorenter 指令),那么每次执行 monitorexit 都会使计数器减一,直到计数器为 0,表示线程完全释放了该对象的监视器锁。


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容