Synchronized是Java中解决并发问题的一种最常用的方法,也是使用相对容易的一种方法。本文主要介绍其实现原理,如果对使用不太清楚的,可以参考java多线程系列02sychronized关键字。
锁实现原理
首先从一段代码开始讲起
1 | package com.paddx.test.concurrent; |
反编译结果:
主要关注的是monitorenter和monitorexit这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter
1 |
|
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit
1 | The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. |
这段话的大概意思为:
- 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor对象来完成,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
另外底层是通过互斥锁来实现的,因此在获取锁的时候会是程序从用户态陷入内核态。
我们再来看一下同步方法的反编译结果,源代码:
1 | package com.paddx.test.concurrent; |
反编译结果:
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
monitor介绍
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
1 | ObjectMonitor() { |
ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList集合,当线程获取到对象的monitor后进入_Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒,如果调用notify或notifyAll将会把_WaitSet移动到_EntryList里,之后参与竞争获取锁。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示:
总结
Synchronized是Java并发编程中最常用的用于保证线程安全的方式,通过monitor来实现,锁的获取和释放主要有以下过程
- 查看monitor对象已被获取,如果已被获取,则判断是否被当前线程获取,是则进入并将将持有的数量加1;如果没有则值获取对象,并将持有数量加1;有则进入队列进行等待
- 将持有数量减1,如果等于0则释放锁,唤醒等待队列上的线程;大于1则不释放锁。
Synchronized不是公平锁,可以试想这样一个情形,假设一个锁刚释放,现在恰好有一个线程来获取锁,检测到锁的owner为空,则其可以直接获取。而释放锁的线程虽然唤醒等待队列上的线程,但是其也要执行整个所得获取过程,这时检测ower不为空,则其只能继续等待。导致先来的线程没有获取到,后来的却获取到对应的锁。从而引发不公平