synchronized介绍
synchronized相信大家都比较熟悉,面试的时候也是必备问题。有很多书籍介绍它,网上也有很多很多相关的文字,我自己也看过很多。但是看了就忘,印象不深,于是我决定阅读源码来加深记忆。代码出真知,这一看不要紧,不仅让我对synchronized有了更深入的理解,我还发现,网上许多文章,甚至很多面试官的理解都是错误的。
比如,说说你对自旋锁的理解。你可以先把答案自己在心里想一下,看到下面,你可能会发现,原来你原来的理解是错误的。
synchronized使用
使用上我就不多说了,相信大家都知道,这里概括一下
synchronized(obj) {
....
some code
...
}
这种使用时对obj对象上锁。也是比较常见的使用。
synchronized void test() {
....
some code
...
}
这种事对this进行上锁
synchronized static void test() {
....
some code
...
}
这种事对Class对象上锁。
synchronized字节码表示
加入我们有一个简单的方法如下
public void test() {
Object obj = new Object();
synchronized (obj) {
System.out.println("in locking");
}
}
会编译成以下字节码
public test()V
...
MONITORENTER
L0
LINENUMBER 15 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "in locking"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L6
LINENUMBER 16 L6
ALOAD 2
MONITOREXIT
L1
GOTO L7
L2
FRAME FULL [com/pinduoduo/service/jmm/JmmTest java/lang/Object java/lang/Object] [java/lang/Throwable]
ASTORE 3
ALOAD 2
MONITOREXIT
...
}
其他我们不要管,可以看到字节码中有一个MONITOREXIT
指令和两个MONITOREXIT
指令。两个MONITOREXIT
是因为要异常的情况也也要进行MONITOREXIT
.
synchronized的jvm表示
我们在上小节知道了synchronized块是有MONITORENTER
和MONITOREXIT
指令包裹着的,那这两个指令怎么执行呢?这就涉及的jvm怎么实现的了,这里我们就以hotspot为例,看看他是怎么实现的。
hotspot的这两个指令执行入口在bytecodeInterpreter.cpp
中的CASE(_monitorenter)
和CASE(_monitorexit)
中,接着就是一堆c++代码了,这里我们不深入,不然大家肯定睡着了,有兴趣的小伙伴可以自己去看看,我其他文章也会具体分析。
synchronized原理
这一小节会简单的说一说synchronized的原理,都是根据我阅读源码的总结。
偏向锁
偏向锁适用于没有多个线程会进入的资源。这个点在说轻量级锁的时候会再次提到。
偏向锁又称无锁,因为它只改变锁对象头的markWord。
在Hotspot中,每个java对象并不是对应一个c++对象,而是用oop-klass这种二分模型表示,关于这个二分模型我们也不展开说。oop中一个一个markOop,他的类型是 markOopDesc*,是一个指针。而markword就是指针的值,而不是指针指向的地址的值。啥意思呢?比如64位机器上,markOop就是一个64位的数值而已,这一点也是我饶了很久才懂。这里只是提一下,不重要。
结构图如下:
偏向锁状态下,markword的锁标志位会变成01,是否偏向标志为1,此外epoch只一个标志,用于是否需要批量重定向的判断,我们先不管它。那如何证明偏向锁状态下,锁的markword会变成这个样子呢?我们的jol就登场了,跟我一起看接下来的小demo,耐心。
首先我们引入jol的依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
代码如下
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
public class MarkWordTest {
public static void main(String[] args) throws InterruptedException {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
MarkWordTest object = new MarkWordTest();
String classLayout1 = ClassLayout.parseInstance(object).toPrintable();
System.out.println("---------------------------加锁前---------------------------");
System.out.println(classLayout1);
System.out.println("------------------------------------------------------------");
synchronized (object) {
//打印当前jvm信息
System.out.println("---------------------------加锁中---------------------------");
String classLayout2 = ClassLayout.parseInstance(object).toPrintable();
System.out.println(classLayout2);
System.out.println("------------------------------------------------------------");
}
System.out.println("---------------------------加锁后---------------------------");
String classLayout3 = ClassLayout.parseInstance(object).toPrintable();
System.out.println(classLayout3);
System.out.println("------------------------------------------------------------");
}
}
首先注意到,代码开始有一个sleep 5秒的操作,是因为偏向锁默认有个延迟启动时间,默认为4s。
然后分别在加锁前、中、后对对象头进行打印,我们看一下打印结果
---------------------------加锁前---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
---------------------------加锁中---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
---------------------------加锁后---------------------------
com.pinduoduo.service.jmm.MarkWordTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------------------------
Process finished with exit code 0
markword是64位,既上面一堆数字的前两行,我们删掉其他东西,只看markword
---------------------------加锁前---------------------------
...
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------
---------------------------加锁中---------------------------
...
0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------
---------------------------加锁后---------------------------
...
0 4 (object header) 05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
...
------------------------------------------------------------
是不是有点看不懂,这是因为,Hopspot是小端储存,既低位值放在高位地址上。那我们按照我们好读的宗旨把它改为大端存
---------------------------加锁前---------------------------
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
------------------------------------------------------------
---------------------------加锁中---------------------------
00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101
------------------------------------------------------------
---------------------------加锁后---------------------------
00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101
------------------------------------------------------------
现在就是我们可以读懂的结构了。
等等,加锁前为啥锁标志位为01,偏向锁模式呢?是的,是这样的,对象头初始就是偏向锁模式,并不是很多文章都说的无锁模式,只是现在还没有指向任何线程。
加锁中我们就好理解了,锁标志位是偏向锁,并且线程id也不是0了。
加锁后可以看到,markword并没有任何改变。这样做自然有用,就是,当有其他线程来想进入临界区的时候,看到偏向线程id不是自己,直接进行锁升级(不考虑批量重偏向)。
至此我们可以得出一个结论:偏向锁只在只有一个线程会进入临界区的场景有用。
不知道你有没有注意到一个问题,就是,既然退出临界区后,markword没有任何变化,那锁重入的计数怎么实现的?这就涉及到synchronized
的重入了,不管是偏向锁、轻量锁还是重量级锁,都是使用的同一套重入机制。
自旋锁的重入
在Hopspot中,每个线程都有一个锁记录表,锁记录是BasicObjectLock
类型,里边有一个obj和displaced_header,obj存储锁对象,set_displaced_header锁对象的markword,每一次进入临界区,都会创建一个BasicObjectLock
对象,如果是重复,那displaced_header就置为null.
轻量级锁
轻量级锁适用于线程交替进入临界区的场景,只要有锁竞争就会升级成重量级锁。注意,1.8中<font color=red>没有自旋锁</font>,别再说经过自旋后升级成重量级锁!!!
别嫌我啰嗦,再比较一下偏向锁与轻量级锁适用场景的不同。
偏向锁:只有一个线程会进入临界区
轻量级锁:不同线程交替进入临界区
偏向锁和轻量级锁都是在没有锁竞争的条件下适用的。
轻量级锁的加锁和释放都是通过cas操作对锁对象的markword就行修改而实现的,它的重入与偏向锁的机制相同,这里就不赘述了。
重量级锁
重量级锁的实现是用了监视器模式(moniter),我猜字节码指令MONITORENTER
、MONITOREXIT
开始指的就是这个操作,后来经过一系列的优化,使这两个指令的字面意义跟锁的实现不太对应了。
在Hotspot中,每个对象(java对象)都会关联一个ObjectMonitor,这个就是一个监视器。
ObjectMonitor中我们重点关注以下几个字段
ObjectMonitor() {
_owner = NULL; // 锁持有者
_recursions = 0; // 重入次数
_WaitSet = NULL;
_cxq = NULL ;
_EntryList = NULL ;
}
_owner
既为锁持有者。
_recursions
表示重入次数。从这个字段就可以看出来,重量级锁的重入跟轻量级锁、偏向锁的重入不同,是用计数法来做的。
_cxq
是一个队列,如果线程获取锁失败,一定是进入这个队列的。
_EntryList
也是一个人队列,当ObjectMonitor
持有者释放锁后,ObjectMonitor
一定是从这个队列中唤醒一个线程的。
_WaitSet
是调用wait
操作的线程队列。
认识这几个字段后,我们会很容易理解ObjectMonitor的运作原来,并没有网上说的那么复杂,至于网上经常晒的那张流程图我就不贴了。下面我概括一下。
- 线程要进入临界区的时候,如果
_owner
为空,则直接用cas去修改_owner
的值,成功则证明获取锁成功,否则,会准备进去队列,在进入队列前,会经过多次自旋尝试cas修改_owner
,这也是为什么synchronized
,是非公平锁的原因,就是他会不断的抢占式的获取锁。 - 多次尝试无果后,线程会进入
_cxq
队列中。注意,抢占无果的线程一定是进入_cxq
队列。 - 线程在获取锁后,如果调用lock#wait方法,那这个线程会进入
_WaitSet
队列。 当获取锁的线程调notify或notifyAll时,根据不同的策略,
_WaitSet
会将节点插入_cxq
或_EntryList
中。0: 将头节点插入到EntryList头部 1: 将头节点插入到EntryList尾部 2: 将头节点插入到cxq头部,默认选项 3: 将头节点插入到cxq尾部
当锁空闲的时候,根据不同的策略,以不同的方式将_cxq
插入到_EntryList
,
QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;
QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
QMode = 0:暂时什么都不做
- 从队列中取线程来进入临界区:
如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回; - 如果是重入则会_recursions++
总结
- 偏向锁适用于只有一个线程会进入临界资源的情景,只要有多线程进入临界区就会导致锁升级
- 轻量级锁适合多线程交替进入临界区的情景,只要有多线程同时想进入临界区就会导致锁升级
- 偏向锁和轻量级锁的重入都是由线程中的锁记录来实现的
- 重量级锁采用
Moniter
实现。根据不同策略和操作,线程在_WaitSet
、_cxq
、_EntryList
中流转,重入通过_recursions计数来实现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。