通过字节码去理解synchronized 关键字
- 我们都知道synchronized是java提供一个关键字,这个关键字可以实现线程的同步保护共享资源。这个保护归根结底是由谁来实现呢?大家应该都知道是jvm,那jvm是如何确保线程安全的呢?本文的重点就是来学习这一块的知识点。下面我们通过几个简单的代码从字节码的角度分析synchronized的原理。
synchronized修饰代码块
public class MySynchronizedTest04 {
Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello world");
}
}
}
- 找到class文件的所在位置执行javap -c 即可查看字节码文件内容,字节码如下所示
public class com.karl.concurrent.syn.MySynchronizedTest04 {
java.lang.Object object;
public com.karl.concurrent.syn.MySynchronizedTest04();
Code:
0: aload_0
1: invokespecial #1
4: aload_0
5: new #2
8: dup
9: invokespecial #1
12: putfield #3
15: return
## 上述就是MySynchronizedTest04的构造方法的字节码,不是我们的讲述重点在这里就不做过多的讲解
public void method();
## 字节码文件中有很多的助记符 不用刻意去背,遇到了再去查看就行
Code:
0: aload_0
1: getfield #3
4: dup
5: astore_1
6: monitorenter ## monitorenter 这一行就是比较重要对应synchronized的语义
7: getstatic #4
10: ldc #5
12: invokevirtual #6
15: aload_1
16: monitorexit ## monitorexit 对应上面的monitorenter 一进一出
17: goto 25 ## 进入25 25是return 方法执行结束 这个是正常的释放锁的过程
## 为保证线程能够释放锁 不管是正常结束还是异常报错都能够释放锁 下面就是出现了异常场景 释放锁的过程。所以monitorenter 一般会对应两个monitorexit 一个正常退出一个异常退出
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指令后,线程就会持有monitor对象,当线程退出monitorexit指令后就会释放monitor对象。这就是获取到锁和释放锁的过程。
synchronized修饰普通的实例方法
public class MySynchronizedTest05 {
public synchronized void method() {
System.out.println("hello world");
}
}
- 通过javap -v 查看更多字节码信息 如下所示
public synchronized void method();
descriptor: ()V ## 无参无返回值
flags: ACC_PUBLIC, ACC_SYNCHRONIZED ## 标志位 中有ACC_SYNCHRONIZED
Code:
## 在code中我们没有发现 monitorenter monitorexit
stack=2, locals=1, args_size=1
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
LineNumberTable:
line 13: 0
line 14: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/karl/concurrent/syn/MySynchronizedTest05;
}
SourceFile: "MySynchronizedTest05.java"
- 当synchronized修饰一个代码块和修饰一个方法的时候,它的实现是不一样的。synchronized在修饰方法的时候是通过方法的标志位flags 是否含有ACC_SYNCHRONIZED 来区分是否为同步方法。
- 当线程准备调用这个方法的时候,首先要坚持当前方法的标志位是否含有ACC_SYNCHRONIZED如果含有ACC_SYNCHRONIZED,当前线程尝试去获取当前对象的锁,获取成功之后再调用该方法,方法执行完毕后释放当前对象的锁。
synchronized修饰静态方法
public class MySynchronizedTest06 {
public synchronized static void method() {
System.out.println("hello world");
}
}
- 通过javap -v 查看更多字节码信息 如下所示
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2
3: ldc #3
5: invokevirtual #4
8: return
LineNumberTable:
line 12: 0
line 13: 8
}
SourceFile: "MySynchronizedTest06.java"
- 通过我们观察和修饰普通方法唯一的区别就是在访问标志位多了ACC_STATIC,表面该方法为静态方法,而且通过上述分析ACC_SYNCHRONIZED可知是同步的静态方法。
- 当线程准备调用这个方法的时候,首先要坚持当前方法的标志位是否含有ACC_SYNCHRONIZED如果含有ACC_SYNCHRONIZED,且含有ACC_STATIC当前线程尝试去获取当前类的锁,获取成功之后再调用该方法,方法执行完毕后释放当前对象的锁。
总结
- jvm的同步是基于进入退出monitor对象(管程)来实现的。每个java对象的实例都会有一个monitor对象,monitor对象与java对象一同创建/销毁。monitor是由C++来管理的。
- 当多线程同时访问同一段代码块时,这些线程会被放到一个entryList集合中,处于阻塞状态的线程都会被放置到该list中。接下来当线程获取到对象的monitor对象时,Monitor是依赖操作系统底层mutex lock锁来实现互斥的。线程获取到mutex lock成功其他线程则无法获取。