上一篇我们讲解了synchronized底层原理,使用了Monitor监视器锁。 如果仅仅从Java层面,我们是看不出来synchronized在多线程竞争锁资源下一个详细的过程。
接下来,我们就研究一下Monitor底层原理,让我们一起分析一下synchronized底层是如何竞争锁资源的。
理解Monitor监视器锁原理
任何一个对象
都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是
基于进入和退出Monitor对象
来实现
方法同步
和
代码块同步
。
虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
1.MonitorEnter和MonitorExit
1.1 MonitorEnter
• monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
(体现可重入锁)
c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
1.2MonitorExit
• monitorexit:执行monitorexit的线程
必须是object_ref所对应的monitor的所有者
。指令执行时,monitor的进入数减1,**如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。**其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
1.2.1 解释monitorexit出现两次
monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
2.为什么只有在同步的代码块或者方法中才能调用wait/notify等方法?
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,
其实wait/notify等方法也依赖于monitor对象
,这就是为什么
只有在同步的代码块或者方法中才能调用wait/notify等方法
,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
Java中的所有对象的顶级父类是Object类,Object 类定义了 wait(),notify(),notifyAll() 方法,底层用的就是monitor机制,这也是为什么synchronize锁的是整个对象。
3.解释ACC_SYNCHRONIZED
看一个同步方法:
package it.yg.juc.sync;
public class SynchronizedMethod {
public synchronized void method() {
System.out.println("Hello World!");
}
}
反编译结果:
从编译的结果来看,方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。
JVM就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
4.小结
同步方法(无论静态或者非静态)是通过方法中的 access_flags 中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过 monitorenter(进入锁) 和 monitorexit(释放锁) 来实现。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的
互斥原语mutex
来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
5.重点:从C++源码分析synchronized底层是如何进行锁资源竞争的
monitor,可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。
5.1ObjectMonitor 底层C++源码
在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor类实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
class ObjectMonitor {
public:
enum {
OM_OK, // no error
OM_SYSTEM_ERROR, // operating system error
OM_ILLEGAL_MONITOR_STATE, // IllegalMonitorStateException
OM_INTERRUPTED, // Thread.interrupt()
OM_TIMED_OUT // Object.wait() timed out
};
...
ObjectMonitor() {
_header = NULL; // 对象头
_count = 0; // 记录该线程获取锁的次数
_waiters = 0, // 当前有多少处于wait状态的thread
_recursions = 0; // 锁的重入次数
_object = NULL;
_owner = NULL; // 指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 存放处于wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 存放处于block锁阻塞状态的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
解释代码中几个关键属性:
_count,记录该线程获取锁的次数(就是前前后后,这个线程一共获取了多少次锁);
_recursions ,锁的重入次数(道格李Lock下的state);
_owner 对应 The Owner,含义是持有ObjectMonitor对象的线程;
_EntryList 对应 Entry List,含义是存放处于block锁阻塞状态的线程队列(多线程下,竞争锁失败的线程会进入EntryList队列);
_WaitSet 对应 Wait Set,含义是存放处于wait状态的线程队列(正在执行代码的线程遇到wait(),会进行WaitSet队列)。
5.2通过CAS尝试把monitor的_owner字段设置为当前线程;
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
// 通过CAS尝试把monitor的_owner字段设置为当前线程
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT recursions == 0 or explicitly set recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
// 设置之前的owner指向当前线程,说明当前线程已经持有锁,此次为重入,_recursions自增
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
// 如果之前_owner指向的BasicLock在当前线程栈上,说明当前线程是第一次进入该monitor
// 设置recursions为1,owner为当前线程,该线程成功获得锁并返回
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
从上面代码中,可以看出,if (cur == Self)),说明设置之前的owner指向当前线程,说明当前线程已经持有锁,此次为重入,_recursions自增,即 synchronized是一把可重入锁
synchronized还是一把非公平锁,新的线程进来是可以有抢占优先级的。
5.3ObjectMonitor中的队列
ObjectMonitor中有两个队列,_EntryList 和_WaitSet ,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
-
若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
6.synchronized获取锁流程(思想)
这个流程很关键:
因为它体现的就是synchronized底层是如何设计【同步策略思想】。 并且,这个思想与接下来道格李写的【AQS的同步策略思想】很相似。
所以,这也是我一直想要突破的地方:
看源码的时候,最关键的就是思考设计者们设计这套框架,所采用的【思想】。退一万步讲,你可以不记得syn的底层c++源码具体实现,你也可以不用记得lock的Java源码底层的详细实现,但是你必须记住:synchronized和lock实现并发安全的【思想】。
结果发现:原来Java界的两个同步利器synchronized和lock,底层实现的思想竟然如此相似。
使用synchronized进行同步。
-
我们都知道如果t0,t1,t2在进行获取锁时,如果t0获取锁资源成功(成功的标志:CAS
成功
把monitor的_owner字段设置为当前线程), - 那么t1和t2是不会立马挂起的,而是先通过CAS自旋的方式再次尝试获取锁,
- 如果失败则入队列(EntryList队列)。
所谓“自旋”,就monitor并不把线程阻塞放入排队队列,而是去执行一个无意义的循环(因为线程阻塞涉及到用户态和内核态切换的问题),循环结束后看看是否锁已释放并直接进行竞争上岗步骤,如果竞争不到继续自旋循环,循环过程中线程的状态一直处于running状态。明显自旋锁使得synchronized的对象锁方式在线程之间引入了不公平。但是这样可以保证大吞吐率和执行效率。
不过虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。所以自旋的次数一般控制在一个范围内,例如10,50等,在超出这个范围后,线程就进入排队队列。
结语:
synchronized底层C++源码分析至此。
给自己最大的感触就是:我是看完AQS底层Java源码后,在接触到道格李解决同步策略的【思想】后,然后突然想到synchronized是如何解决同步的思想的,继而又研究了synchronized底层是如何解决同步问题的。
结果发现,二者有着惊人的相似之处:
1.多线程竞争锁,当某个线程竞争成功,其他线程都是先自旋,如果失败,然后入队列
2.竞争锁成功的线程,都会被记录在一个变量中,这个变量返回的就是当前竞争锁成功的线程对象;
3.可重入锁状态都是通过一个变量记录的;
那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局。