synchronized 锁机制在 Java 虚拟机中的同步是基于进入和退出监视器锁对象 monitor 实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor 对象,当一个 monitor 被某个线程持有后,它便处于锁定状态。在 HotSpot 虚拟机中,monitor 是由 ObjectMonitor 实现的,每个等待锁的线程都会被封装成 ObjectWaiter 对象,ObjectMonitor 中有两个集合,_WaitSet 和 _EntryList,用来保存 ObjectWaiter 对象列表 ,owner 区域指向持有 ObjectMonitor 对象的线程。当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合尝试获取 moniter,当线程获取到对象的 monitor 后进入 _Owner 区域并把 _owner 变量设置为当前线程,同时 monitor 中的计数器 count 加1;若线程调用 wait() 方法,将释放当前持有的 monitor,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor 并复位变量的值,以便其他线程获取 monitor。
_EntryList:存储处于 Blocked 状态的 ObjectWaiter 对象列表。
_WaitSet:存储处于 wait 状态的 ObjectWaiter 对象列表。
首先来看一个例子,深入JVM看字节码,创建如下的代码:
public class SynchTestDemo {
public static void printlnTest(){
synchronized (SynchTestDemo.class){
System.out.println("hhhhh");
}
}
public static void main(String[] args) {
printlnTest();
}
}
使用javac命令进行编译生成.class文件:
javac SynchTestDemo.java
使用javap命令反编译查看.class文件的信息
javap -verbose SynchTestDemo.class
执行完毕可以得到如下信息,如图所示:
请红色方框里的monitorenter
和monitorexit
。其中monitorenter指令执行;了一次,而monitorexit指令实际上是执行了两次,第一次是正常情况下释放锁,第二次为发生异常情况时释放锁,这样做的目的在于保证线程不死锁。
Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得。
在JVM规范中有提到对monitorenter
指令的描述: 任何一个对象都有一个monitor与其相关联,当且有一个monitor被持有后,它将处于锁定的状态,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,他会尝试去获取当前对应的monitor的所有权。
一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
在JVM规范中同样有提到对monitorenter
指令的描述:能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程;执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
synchronized关键字被编译成字节码后会被翻译成monitorenter
和monitorexit
两条指令分别在同步块逻辑代码的起始位置与结束位置,如下图所示:
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
synchronized的实现原理:synchronized的底层实际是通过一个monitor对象来实现的,其实wait/notify方法也是依赖于monitor对象来实现的,这就是为什么只有在同步代码块或者方法中才能调用该方法,否则就会抛出出java.lang.IllegalMonitorStateException的异常的原因。再看一个例子:
public class SynchTestDemo {
public synchronized void printlnTest(){
System.out.println("hhhhh");
}
public static void main(String[] args) {
new SynchTestDemo().printlnTest();
}
}
根据上面的方法编译运行然后反编译获取字节码文件,可得下面的内容:
从字节码反编译的可以看出,同步方法并没有通过指令monitorenter和monitorexit来实现的,但是相对于普通方法来说,其常量池多了了 ACC_SYNCHRONIZED 标示符。JVM实际就是根据该标识符来实现方法的同步的。
当方法被调用时,会检查ACC_SYNCHRONIZED标志是否被设置,若被设置,线程会先获取monitor,获取成功才能执行方法体,方法执行完成后会再次释放monitor。在方法执行期间,其他线程都无法获得同一个monitor对象。
其实两种同步方式从本质上看是没有区别的,两个指令的执行都是JVM调用操作系统的互斥原语mutex来实现的,被阻塞的线程会被挂起、等待重新调度,会导致线程在“用户态”和“内核态”进行切换,就会对性能有很大的影响。
monitor通常被描述为一个对象,可以将其理解为一个同步工具,或者可以理解为一种同步机制。所有的Java对象自打new出来的时候就自带了一把锁,就是monitor锁,也就是对象锁,存在于对象头(Mark Word),锁标识位为10,指针指向的是monitor对象起始地址。
在Java虚拟机(HotSpot)中,Monitor是由其底层实际是由C++对象ObjectMonitor实现的:
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; // 线程的重入次数
_object = NULL; // 存储该monitor的对象
_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;
}
举个例子具体分析一下_cxq队列与_EntryList队列的区别:
public void print() throws InterruptedException {
synchronized (obj) {
System.out.println("Hello World");
//obj.wait();
}
}
若多线程执行上面这段代码,刚开始t1线程第一次进同步代码块,能够获得锁,之后马上又有一个t2线程也准备执行这段代码,t2线程是没有抢到锁的,t2这个线程就会进入_cxq这个队列进行等待,此时又有一个线程t3准备执行这段代码,t3当然也会没有抢到这个锁,那么t3也就会进入_cxq进行等待。
接着,t1线程执行完同步代码块把锁释放了,这个时候锁是有可能被t1、t2、t3中的任何一个线程抢到的。
假如此时又被t1线程给抢到了,那么上次已经进入_cxq这个队列进行等待的线程t2、t3就会进入_EntryList进行等待,若此时来了个t4线程,t4线程没有抢到锁资源后,还是会先进入_cxq进行等待。
具体分析一下_WaitSet队列与_EntryList队列:(图片来源微信公众号 - 得物技术:精选文章|深入理解synchronzied底层原理 )
每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。ObjectWaiter 对象里存放thread(线程对象) 和unpark的线程, 每一个等待锁的线程都会有一个ObjectWaiter对象,而objectwaiter是个双向链表结构的对象。
结合上图monitor的结构图可以分析出,当线程的拥有者执行完线程后,会释放锁,此时有可能是阻塞状态的线程去抢到锁,也有可能是处于等待状态的线程被唤醒抢到了锁。在JVM中每个等待锁的线程都会被封装成ObjectMonitor对象,_owner标识拥有该monitor的线程,而_EntryList和_WaitSet就是用来保存ObjectWaiter对象列表的,_EntryList和_WaitSet最大的区别在于前者是用来存放等待锁block状态的线程,后者是用来存放处于wait状态的线程。
当多个线程同时访问同一段代码时:
monitor对象存在于每个Java对象的对象头(Mark Word)中,所以Java中任何对象都可以作为锁,由于notify/notifyAll/wait等方法会使用到monitor锁对象,所以必须在同步代码块中使用。
多线程情况下,线程需要同时访问临界资源,监视器monitor可以确保共享数据在同一时刻只会有一个线程在访问。
可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
public class SynchronizedDemo {
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method1();
}
private synchronized void method1() {
System.out.println(Thread.currentThread().getId() + ": method1()");
method2();
}
private synchronized void method2() {
System.out.println(Thread.currentThread().getId()+ ": method2()");
method3();
}
private synchronized void method3() {
System.out.println(Thread.currentThread().getId()+ ": method3()");
}
}
执行monitorenter获取锁
执行monitorexit命令