synchronized 对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
synchronized 关键字底层原理属于 JVM 层面。
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SynchronizedDemo.class 反编译。
public class javase.thread.SynchronizedDemo {
public javase.thread.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String synchronized 代码块
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
}
从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor 对象存在于每个 Java 对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么 Java 中任意对象可以作为锁的原因) 的持有权。当计数器为 0 则可以成功获取,获取后将锁计数器设为 1 也就是加 1。相应的在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。如果有可重入的情况,锁计数器会持续增加。
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
同样执行上述操作获取字节码。
{
public javase.thread.SynchronizedDemo2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljavase/thread/SynchronizedDemo2;
public static synchronized void method();
descriptor: ()V
flags: (0x0029) 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 synchronized 方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
}
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法(注意:要出现这个标识,javap 指令必须加 -v 参数,不然显示不完全),JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
总结:Java 虚拟机中的 synchronized 是基于进入和退出 monitor 对象实现的,同步分为显式同步和隐式同步,同步代码块代表着显式同步,指的是有明确的 monitorenter 和 monitorexit 指令。同步方法代表着隐式同步,同步方法是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
在 HotSpot 虚拟机中,对象在内存中存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding),这里主要关注对象头。
HotSpot 虚拟机的对象头包括两部分(非数组对象)信息,如下图所示:
Mark Word 在不同的锁状态下存储的内容不同,在 32 位 JVM 中是这么存的:
重量级锁就是 synchronized 锁,锁的标记位为 10,其中指针指向 monitor 对象的起始地址。每一个对象都存在一个 monitor 与之相关联,monitor 对象可以与对象一起创建销毁或者当线程试图获取对象锁的时候自动生成,monitor 被线程持有之后就处于锁定的状态。JVM 中 monitor 是有 ObjectMonitor 实现的,其主要的数据结构为:
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 指向只有monitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
monitor 对象中有内部有两个队列,一个用来保存一个 ObjectWaiter 对象列表(每个等待这个锁的线程都会被封装成为ObjectWaiter对象),owner 指向的是持有持有 monitor 对象的线程,当多个线程同时访问一段代码的时候,首先会进入 _Entry_list 集合,当线程获取到对象的 Monitor 后,会进入 _owner 区域并把 monitor 中的 owner 设置为当前线程,同时 monitor 中的计数器 count 加 1,如果线程调用 wait() 方法,将会释放当前持有的 monitor,owner 设置为 null, count 减 1,同时该线程进入 WaitSet 集合中等待被唤醒,如果当前线程执行完毕也将会释放 monitor 锁,owner 变为null,count 减 1。
monitor 对象存在每个 Java 对象的对象头中 (MarkWord 结构中存储了指向 monitor 对象的指针),也就是为什么 notify,notifyall,wait 方法都存在 Object 对象中的原因。
总结 synchronized 修饰方法可以看出,JVM 通过判断 ACC_SYNCHRONIZED 访问标志来判别一个方法是否是同步方法,进而获取 monitor 对象,在早期 synchronized 是重量锁效率低,因为 monitor 是依赖底层操作系统来实现的,而操作系统实现线程之间的转换需要从用户太转换到核心态,这个转换要浪费很多时间,后来从 JVM 层面对 synchronized 有了很大的优化,为了减少获得锁或者是释放锁的性能消耗,引入了轻量级锁和偏向锁,所以现在的 synchronized 还可以。