死磕Synchronized底层实现--概论
farmerjohngit opened this issue · 71 comments
关于synchronized
的底层实现,网上有很多文章了。但是很多文章要么作者根本没看代码,仅仅是根据网上其他文章总结、照搬而成,难免有些错误;要么很多点都是一笔带过,对于为什么这样实现没有一个说法,让像我这样的读者意犹未尽。
本系列文章将对HotSpot的synchronized
锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized
路上的同学一些帮助。主要包括以下几篇文章:
更多文章见个人博客:https://github.com/farmerjohngit/myblog
大概花费了两周的实现看代码(花费了这么久时间有些忏愧,主要是对C++、JVM底层机制、JVM调试以及汇编代码不太熟),将synchronized
涉及到的代码基本都看了一遍,其中还包括在JVM中添加日志验证自己的猜想,总的来说目前对synchronized
这块有了一个比较全面清晰的认识,但水平有限,有些细节难免有些疏漏,还望请大家指正。
本篇文章将对synchronized
机制做个大致的介绍,包括用以承载锁状态的对象头、锁的几种形式、各种形式锁的加锁和解锁流程、什么时候会发生锁升级。需要注意的是本文旨在介绍背景和概念,在讲述一些流程的时候,只提到了主要case,对于实现细节、运行时的不同分支都在后面的文章中详细分析。
本人看的JVM版本是jdk8u,具体版本号以及代码可以在这里看到。
synchronized简介
Java中提供了两种实现同步的基础语义:synchronized
方法和synchronized
块, 我们来看个demo:
public class SyncTest {
public void syncBlock(){
synchronized (this){
System.out.println("hello block");
}
}
public synchronized void syncMethod(){
System.out.println("hello method");
}
}
当SyncTest.java被编译成class文件的时候,synchronized
关键字和synchronized
方法的字节码略有不同,我们可以用javap -v
命令查看class文件对应的JVM字节码信息,部分信息如下:
{
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // monitorenter指令进入同步块
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // monitorexit指令退出同步块
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // monitorexit指令退出同步块
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
public synchronized void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String hello method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
从上面的中文注释处可以看到,对于synchronized
关键字而言,javac
在编译时,会生成对应的monitorenter
和monitorexit
指令分别对应synchronized
同步块的进入和退出,有两个monitorexit
指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac
为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit
命令释放锁。而对于synchronized
方法而言,javac
为其生成了一个ACC_SYNCHRONIZED
关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED
修饰,则会先尝试获得锁。
在JVM底层,对于这两种synchronized
语义的实现大致相同,在后文中会选择一种进行详细分析。
因为本文旨在分析synchronized
的实现原理,因此对于其使用的一些问题就不赘述了,不了解的朋友可以看看这篇文章。
锁的几种形式
传统的锁(也就是下文要说的重量级锁)依赖于系统的同步函数,在linux上使用mutex
互斥锁,最底层实现依赖于futex
,关于futex
可以看我之前的文章,这些同步函数都涉及到用户态和内核态的切换、进程的上下文切换,成本较高。对于加了synchronized
关键字但运行时并没有多线程竞争,或两个线程接近于交替执行的情况,使用传统锁机制无疑效率是会比较低的。
在JDK 1.6之前,synchronized
只有传统的锁机制,因此给开发者留下了synchronized
关键字相比于其他同步机制性能不好的印象。
在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。
在看这几种锁机制的实现前,我们先来了解下对象头,它是实现多种锁机制的基础。
对象头
因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。一种很直观的方法是,用一个全局map,来存储这个映射关系,但这样会有一些问题:需要对map做线程安全保障,不同的synchronized
之间会相互影响,性能差;另外当同步对象较多时,该map可能会占用比较多的内存。
所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。
在JVM中,对象在内存中除了本身的数据外还会有个对象头,对于普通对象而言,其对象头中有两类信息:mark word
和类型指针。另外对于数组而言还会有一份记录数组长度的数据。
类型指针是指向该对象所属类对象的指针,mark word
用于存储对象的HashCode、GC分代年龄、锁状态等信息。在32位系统上mark word
长度为32bit,64位系统上长度为64bit。为了能在有限的空间里存储下更多的数据,其存储格式是不固定的,在32位系统上各状态的格式如下:
可以看到锁信息也是存在于对象的mark word
中的。当对象状态为偏向锁(biasable)时,mark word
存储的是偏向的线程ID;当状态为轻量级锁(lightweight locked)时,mark word
存储的是指向线程栈中Lock Record
的指针;当状态为重量级锁(inflated)时,为指向堆中的monitor对象的指针。
重量级锁
重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。
重量级锁的状态下,对象的mark word
为指向一个堆中monitor对象的指针。
一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。
其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。
如果一个线程在同步块中调用了Object#wait
方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。
以上只是对重量级锁流程的一个简述,其中涉及到的很多细节,比如ObjectMonitor对象从哪来?释放锁时是将cxq中的元素移动到EntryList的尾部还是头部?notfiy时,是将ObjectWaiter移动到EntryList的尾部还是头部?
关于具体的细节,会在重量级锁的文章中分析。
轻量级锁
JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record
,其包括一个用于存储对象头中的 mark word
(官方称之为Displaced Mark Word
)以及一个指向对象的指针。下图右边的部分就是一个Lock Record
。
加锁过程
1.在线程栈中创建一个Lock Record
,将其obj
(即上图的Object reference)字段指向锁对象。
2.直接通过CAS指令将Lock Record
的地址存储在对象头的mark word
中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record
第一部分(Displaced Mark Word
)为null,起到了一个重入计数器的作用。然后结束。
4.走到这一步说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj
字段等于当前锁对象的Lock Record
。
2.如果Lock Record
的Displaced Mark Word
为null,代表这是一次重入,将obj
设置为null后continue。
3.如果Lock Record
的Displaced Mark Word
不为null,则利用CAS指令将对象头的mark word
恢复成为Displaced Mark Word
。如果成功,则continue,否则膨胀为重量级锁。
偏向锁
Java是支持多线程的语言,因此在很多二方包、基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如synchronized
这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo:
import java.util.ArrayList;
import java.util.List;
public class SyncDemo1 {
public static void main(String[] args) {
SyncDemo1 syncDemo1 = new SyncDemo1();
for (int i = 0; i < 100; i++) {
syncDemo1.addString("test:" + i);
}
}
private List<String> list = new ArrayList<>();
public synchronized void addString(String s) {
list.add(s);
}
}
在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized
的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。
在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。
对象创建
当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word
将是可偏向状态,此时mark word中
的thread id(参见上文偏向状态下的mark word
格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
加锁过程
case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word
中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。
case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word
为空的Lock Record
中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized
关键字带来的性能开销基本可以忽略。
case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint
中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word
改为无锁状态(unlocked),之后再升级为轻量级锁。
由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。
解锁过程
当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record
来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record
的obj
字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。
下图展示了锁状态的转换流程:
另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0
来关闭延迟。
批量重偏向与撤销
从上文偏向锁的加锁解锁过程中可以看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point
时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁。safe point
这个词我们在GC中经常会提到,其代表了一个状态,在该状态下所有线程都是暂停的(大概这么个意思),详细可以看这篇文章。总之,偏向锁的撤销是有一定成本的,如果说运行时的场景本身存在多线程竞争的,那偏向锁的存在不仅不能提高性能,而且会导致性能下降。因此,JVM中增加了一种批量重偏向/撤销的机制。
存在如下两种情况:(见官方论文第4小节):
1.一个线程创建了大量对象并执行了初始的同步操作,之后在另一个线程中将这些对象作为锁进行之后的操作。这种case下,会导致大量的偏向锁撤销操作。
2.存在明显多线程竞争的场景下使用偏向锁是不合适的,例如生产者/消费者队列。
批量重偏向(bulk rebias)机制是为了解决第一种场景。批量撤销(bulk revoke)则是为了解决第二种场景。
其做法是:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch
字段,每个处于偏向锁状态对象的mark word中
也有该字段,其初始值为创建该对象时,class中的epoch
的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch
字段改为新值。下次获得锁时,发现当前对象的epoch
值和class的epoch
不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word
的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
End
Java中的synchronized
有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁 的顺序升级。JVM种的锁也是能降级的,只不过条件很苛刻,不在我们讨论范围之内。该篇文章主要是对Java的synchronized
做个基本介绍,后文会有更详细的分析。
关于 锁 能不能降级的问题;我在《java并发编程的艺术》这本书中2.2.2节的第四行中看到,
“锁可以升级,但是不能降级”,与这篇文章的End里面的描述有点矛盾,想再次请教一下,谢谢啦
在32位系统上mark word
长度为32字节,64位系统上长度为64字节 ### bytes->bits
@JoseK-43326 这是个好问题,先说结论,在openjdk的hotsopt jdk8u里是有锁降级的机制的,锁降级是什么时候加入到hotspot的这个我没去关注,所以我只说看过代码的jdk8u版本,另外根据R大的这个回答,我相信sunj dk也一样。
然后再详细说:
锁降级的代码在deflate_idle_monitors
方法中,其调用点在进入SafePoint的方法SafepointSynchronize::begin()
中。
在deflate_idle_monitors
中会找到已经idle
的monitor(也就是重量级锁的对象),然后调用deflate_monitor
方法将其降级。
因为锁降级是发生在safepoint的,所以如果降级时间过长会导致程序一直处于STW的阶段。在这里有篇文章讨论了优化机制。jdk8中本身也有个MonitorInUseLists
的开关,其影响了寻找idle monitor
的方式,对该开关的一些讨论看这里。
至于为什么《java并发编程的艺术》中说锁不能降级,我猜测可能该书作者看的jdk版本还没有引入降级机制。
@Geker 感谢指正
@farmerjohngit 感谢指教
您好!想请问一下,我看网上很多讲偏向锁的文章会把偏向锁的撤销和释放作为一个概念;还有的说偏向锁只有撤销锁的操作,没有释放锁的操作;您的文章里强调了释放和撤销的不同,请问您一下,关于这个您是怎么理解的,怎么去区分这两个操作?以及什么时候会触发这两个操作呐?期待您的回答 谢谢,还有简书里的也是我问的 您回答一个就可以啦谢谢
@gallant7 在偏向锁一文中已经说了:
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
触发时机:
释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。
synchronized不是重量级锁吗?它是有释放锁的过程的。是不是撤销锁的时候将对象头变为无锁态时是释放锁的过程?
synchronized不是重量级锁吗?它是有释放锁的过程的。是不是撤销锁的时候将对象头变为无锁态时是释放锁的过程?
synchronized有偏向锁、轻量级锁和重量级锁啊。。。 建议你先把文章看一遍吧
你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁;
想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢
@LYoGA
偏向锁的诞生背景是JVM的开发人员发现在很多场景下,加了synchronized的方法或代码块在运行过程中其实是单线程使用(比如一些工具类为了保证其提供方法的线程安全会加synchronized,但在我们的应用中可能就是单线程使用),所以JVM为了提高性能加入了偏向锁机制。
而你说的当mark word置为无锁状态之后
,就代表该锁对象已经被多个线程使用,已经不满足偏向锁的适用场景了。如果下次获得锁的时候还设置为偏向锁,那可能会有频繁的锁状态切换,导致性能比重量级锁还低。
大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?
大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?
这个去看字节码生成的原理就能知道。
@gezhiwei8899 google下 很多文章
大佬厉害,如何把jdk的源码导入ide 打断掉调试有文章介绍么?
这个去看字节码生成的原理就能知道。
所错了是jvm源码,怎么导入 Clion 这个IDE工具
@farmerjohngit 大佬是不是C++ 也玩的很6
@gezhiwei8899 c++不怎么会,所以我也没倒入过ide,直接用gdb调试的。
请教一下, 偏向锁释放的时候具体做了什么呢?
另外,[是否偏向锁]这个标志位的具体含义一直没搞透彻,jvm如果配置了开启偏向锁就是1没有配置开启就是0么?
大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了?
那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
这句我很难理解,也就是对象一创建出来就直接没hashcode吗?
大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了?
那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
这句我很难理解,也就是对象一创建出来就直接没hashcode吗?
HotSpot JVM在第一次调用Object.hashCode或System.identityHashCode时计算身份hashCode,并将其存储在对象头中。随后的调用只是从头中提取以前计算的值。
hi,
轻量级锁 解锁过程 1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
请教下,这个遍历是具体怎么遍历呢?那么多栈帧
因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。
为什么只需要将最近的lock record设置为null 就可以了啊?
作者你好,我之前看了你的系列文章受益匪浅,现在在知乎看到有人把你的文章搬运过去了没有说明是转发,不知道是不是你本人?知乎链接地址:https://zhuanlan.zhihu.com/p/76794925
@ValiantYuan 不是我。。 太无耻了,转发出处都不保留
一直有个问题,某个对象锁因为线程竞争激烈升级为重量级锁,之后某一段时间没有线程竞争了,这个锁的对象头的锁标志位会变成什么状态?①01-无锁状态,下次再有线程来重新偏向锁-->轻量级锁-->重量级锁 。还是 ②10-重量级锁状态,下次再有线程来直接获取重量级锁。
楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.
@gallant7 在偏向锁一文中已经说了:
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
触发时机:
释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。
我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?
楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.
你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?
你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁;
想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢
同问。
楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.
你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?
我也是同样的问题,请问你们理解了吗
楼主你好, 有一个疑问, 关于批量重偏向的, 上文有提到
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id 改成当前线程Id。
这一句epoch值和class的epoch不相等, 就代表已经偏向了其他线程有些不明白, 正常来看, 在发生批量重偏向时, 锁对象对应的class和该class对应的加有偏向锁对象mark word中的epoch同时+1, 这里无关线程, 只是所有该class的偏向锁对象, 那么这两个epoch应该是相等的, 又怎么会出现后边不相等就偏向了其他线程的结论呢??希望作者解下惑.
你好,我理解的也是在发生批量重偏向的时候两者是相等的,所以不是很理解为什么会在下次获取锁的时候出现不等的情况。请问你搞明白了吗?
我也是同样的问题,请问你们理解了吗
批量重偏向中,epoch自增针对的是 klass 和 被当前存活的thread持有的Oop锁对象。而还存在一种锁对象是:**在批量重偏向时,没有被任何thread持有(也就是当前没有thread在执行对应的synchronize代码),但之前被thread持有过。所以,这种锁对象的markword是偏向状态的,但它的epoch与klass的epoch不相等。**在下一次其他thread准备持有它时,不会因为当前thread的threadId和锁对象markword中的threadId不同而升级为轻量级锁,而是直接CAS成偏向当前thread的markWord(因为锁对象的epoch与klass的epoch不同),从而达到批量重偏向的优化效果。
作者你好,关于偏向锁加锁过程的case2,有这样一个描述:
"case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。"
其中,这一句“会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中”,我有疑问,不知您是否可以交流解答一下:
在看了偏向锁的源码之后,发现锁已经偏向了当前线程,没有做任何处理,源代码如下:
CASE(_monitorenter): {
...
// 如果各bit全为0,表示锁对象的偏向锁已经被当前线程获取
if (anticipated_bias_locking_value == 0) {
// already biased towards this thread, nothing to do
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
// 获取成功,不做任何处理
success = true;
}
所以,想请教下,您此处所述的处理Lock Record的源代码是在哪个位置,我估计是我漏掉了。
先谢谢啦!
@gallant7 在偏向锁一文中已经说了:
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
触发时机:
释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。
作者你好,这边的‘释放’之后另一个线程再次进来是偏向锁? 还是轻量锁?
偏向锁加锁case2:"当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码"
这里会加往栈中加Lock Record吗。我看后面的源码解析没有啊
因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。
为什么只需要将最近的lock record设置为null 就可以了啊?
因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。
因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。
为什么只需要将最近的lock record设置为null 就可以了啊?
因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。
只不过我真的没找到到底什么时候创建的 Lock Record
因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。
为什么只需要将最近的lock record设置为null 就可以了啊?
因为在偏向锁的情况下每次锁重入和第一次尝试访问临界区时创建的 Lock Record 都只有 obj 字段有值(指向锁对象),而 lock 字段(等价于 displaced header)是没有值的(为 null),即每一个 Lock Record 都是等价的,那么我只需要取最近的一个 Lock Record 释放就好了,没有必要遍历到最后取最高位那个 Lock Record 释放。
重新看了一遍后发现解释得有问题,主要原因还是偏向锁每一次加锁都从低到高找最后一个空闲的 Lock Record 管理,那么锁重入的时候必定关联的 Lock Record 会在第一次加锁时关联的 Lock Record 之前(前提是不会在后面分配新的空闲的 Lock Recrod),所以锁释放的时候也从低到高取第一个 Lock Record 释放就好,满足锁重入时的对应关系,后加锁的先释放。
@LYoGA 你好~文中描述到:如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁;想问下,当mark word置为无锁状态之后,下次线程获得锁的时候直接就升级成为轻量级锁了么,为什么不是还是偏向锁呢
我的理解是只有对象是可偏向状态时,才会进入偏向锁状态!所以,在JVM开启了偏向锁模式后,一个新创建的对象应该都是可偏向状态,即标志位是 1-01。如果偏向锁撤销,标志位变成了0-01,说明这个对象已经不能使用偏向锁了,此时的情形和JVM关闭偏向锁模式之后的情形相类似。
问个小问题:lock record的存储位置是哪,是栈吗,还是哪。
为什么图片显示不了呢?
大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了?
那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
这句我很难理解,也就是对象一创建出来就直接没hashcode吗?
当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
大佬,看了几篇文章,一直有个疑问,就是hashcode去哪了?
那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。
这句我很难理解,也就是对象一创建出来就直接没hashcode吗?当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
hashcode并不是对象实例化完就计算好的,是调用计算之后放在mark word里的。
@gallant7 在偏向锁一文中已经说了:
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
触发时机:
释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?
释放的时候就清除的话,下一个线程进来,就不知道这个对象曾经偏向过了。 下一个线程看到这个对象曾经偏向过,就会去清除,然后升级成轻量级锁。 而不是自己变成这个对象的新的偏向锁。当有两个线程锁过一个对象的时候,就已经不适合使用偏向锁了。
解锁过程的过程为什么会发生竞争,不理解
据我所知,Java的方法调用会把当前方法打包成一个栈帧压入线程的虚拟机栈中,方法执行完之后栈帧出栈,如下图的位置
文章说的“在当前的线程的栈帧中创建一个Lock Record”(如下图),请问一下这个栈帧是位于上图的哪一个区域。
但是下图又说是当前线程栈中创建一个Lock Record,这里是否有一处是笔误呢,如果下图是正确的,当前线程栈指的是图一的线程的虚拟机栈吗。
第二个问题:是不是遇到Synchronized关键字就会执行_monitorenter
指令?
最后一个问题:如果只要遇到Synchronized关键字都会执行_monitorenter
指令,那下图红框部分代码应该是不管偏向锁、轻量锁或重量锁应该都会执行吧,这里注释code1说“找到一个空闲的Lock Record”,那也就是说不管偏向、轻量或重量锁都会先找一下有没有空闲的Lock Record? 这块代码关于Lock Record的关系使我很疑惑,轻量锁的时候不是遇到Synchronized都会创建一个Lock Record吗。
对于 JVM 我感觉,没必要了解的这么深入,尤其是对这些锁的处理流程,
个人感觉知道个大概,,能过面试就行了,,,毕竟 Java 框架 还有那么多要学,,吃透了 Spring 全家桶,你也很厉害了。
我感觉自己的记忆力比一般人差,没有真正理解的东西我一段时间之后就忘了,所以希望可以真正吃透它 @binaryCodeSequence
请问一下,对于偏向锁的加锁过程:
case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。
对应下一篇文章偏向锁的code5:如果偏向的线程是自己不是什么也不做吗
@gezhiwei8899 google下 很多文章
@gallant7 在偏向锁一文中已经说了:
这里说的撤销是指在获取偏向锁的过程因为不满足条件导致要将锁对象改为非偏向锁状态;释放是指退出同步块的过程,释放锁的逻辑会在下一小节阐述。请读者注意本文中撤销与释放的区别。
触发时机:
释放:对应的就是synchronized方法的退出或synchronized块的结束。
撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候。我也一直在想,偏向锁释放时,为什么占用线程不把对象头置为无锁状态呢?而是要等下一个线程加锁时才来清除。这是基于怎样的考量呢?
释放的时候就清除的话,下一个线程进来,就不知道这个对象曾经偏向过了。 下一个线程看到这个对象曾经偏向过,就会去清除,然后升级成轻量级锁。 而不是自己变成这个对象的新的偏向锁。当有两个线程锁过一个对象的时候,就已经不适合使用偏向锁了。
如果释放的时候就把对象头设置成无锁状态,下一次获取锁的线程如果刚好是释放锁的线程的话就没法进行重入了,需要再通过一次CAS获取锁,这样违背了偏向锁的设计初衷。
请问重偏向的情况下,原锁对象需要先经过撤销流程吗
作者您好,文章读下来解开了很多疑惑,但还是有一个小问题始终想不明白。当线程 A 持有了偏向锁,在执行同步代码块时线程 B 来竞争,竞争 CAS 如下(位于 bytecodeInterpreter.cpp):
Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header,想问此处 lockee->mark_addr() 与 header 既然都为线程 A 设置的 mark ward,为何会竞争失败进入撤销流程呢?
大佬图片显示不了
偏向锁里应该没有锁记录吧,锁记录是在轻量级锁和重量级锁中才存在的
楼主您好,图片看不到了,我已经打开代理了但是还是看不到,请问有解决方案吗
偏向锁里应该没有锁记录吧,锁记录是在轻量级锁和重量级锁中才存在的
只要解析执行了monitorenter指令,就会寻找一个处于空闲状态的Lock Record
您好,我有一个疑惑,如果方便的话可以解答一下吗?文中说批量重偏向是为了解决“一个线程创建了大量对象并执行了初始的同步操作,另一个线程又把这些对象作为锁”的情况。可是这段话的前半句所做的,不就是将对象初始化为匿名偏向状态吗?那么这个时候,另外的线程来申请加锁,在bytecodeInterpreter的源码中,它并不会进行偏向撤销啊,直接就是在处理匿名偏向的那个分支里做CAS操作加锁了。那没有偏向撤销,就更不可能到达BiasedLocking::revoke()中触发批量重偏向了,所以这种因果关系好像就不能成立了哎
大佬,图片显示不了
偏向锁的释放和轻量级锁的释放都是走的同一地方的代码,为什么偏向锁的描述是“将栈中的最近一条lock record的obj字段设置为null”,而轻量级锁的描述是“遍历所有obj字段等于当前锁对象的Lock Record”,这里描述是不是有问题。源码中的 UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1); 指令是不是表示退出当前的 monitorexit 指令,继续执行下一条指令。所以轻量级锁其实也是只是释放最近一条的 lock record 呢?
您好,我有一个疑惑,如果方便的话可以解答一下吗?文中说批量重偏向是为了解决“一个线程创建了大量对象并执行了初始的同步操作,另一个线程又把这些对象作为锁”的情况。可是这段话的前半句所做的,不就是将对象初始化为匿名偏向状态吗?那么这个时候,另外的线程来申请加锁,在bytecodeInterpreter的源码中,它并不会进行偏向撤销啊,直接就是在处理匿名偏向的那个分支里做CAS操作加锁了。那没有偏向撤销,就更不可能到达BiasedLocking::revoke()中触发批量重偏向了,所以这种因果关系好像就不能成立了哎
@Parker1995 不是的哦,前半句所做的有两个步骤,1是新建对象,2是执行初始的同步操作。当对象在新建之后,此时的对象就已经是匿名偏向状态了,是不需要额外操作的。只要开启了偏向锁模式,那么新建的对象都是匿名偏向状态。关闭了偏向锁模式那么新建的对象都是无锁状态。第二步,执行初始的同步操作的意思是这些新建对象,在线程1中就执行了同步代码,也就是说进行了加锁 、解锁操作。那么前半句的步骤执行完毕之后,这些对象就已经全是偏向锁的状态了,偏向线程1.然后线程2又对这些对象执行同步代码,那么就会触发锁撤销导致计数器++,最后达到阈值,触发批量重偏向。
您好,我有一个疑惑,如果方便的话可以解答一下吗?文中说批量重偏向是为了解决“一个线程创建了大量对象并执行了初始的同步操作,另一个线程又把这些对象作为锁”的情况。可是这段话的前半句所做的,不就是将对象初始化为匿名偏向状态吗?那么这个时候,另外的线程来申请加锁,在bytecodeInterpreter的源码中,它并不会进行偏向撤销啊,直接就是在处理匿名偏向的那个分支里做CAS操作加锁了。那没有偏向撤销,就更不可能到达BiasedLocking::revoke()中触发批量重偏向了,所以这种因果关系好像就不能成立了哎
@Parker1995 不是的哦,前半句所做的有两个步骤,1是新建对象,2是执行初始的同步操作。当对象在新建之后,此时的对象就已经是匿名偏向状态了,是不需要额外操作的。只要开启了偏向锁模式,那么新建的对象都是匿名偏向状态。关闭了偏向锁模式那么新建的对象都是无锁状态。第二步,执行初始的同步操作的意思是这些新建对象,在线程1中就执行了同步代码,也就是说进行了加锁 、解锁操作。那么前半句的步骤执行完毕之后,这些对象就已经全是偏向锁的状态了,偏向线程1.然后线程2又对这些对象执行同步代码,那么就会触发锁撤销导致计数器++,最后达到阈值,触发批量重偏向。
谢谢解答🤝
偏向锁的释放和轻量级锁的释放都是走的同一地方的代码,为什么偏向锁的描述是“将栈中的最近一条lock record的obj字段设置为null”,而轻量级锁的描述是“遍历所有obj字段等于当前锁对象的Lock Record”,这里描述是不是有问题。源码中的 UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1); 指令是不是表示退出当前的 monitorexit 指令,继续执行下一条指令。所以轻量级锁其实也是只是释放最近一条的 lock record 呢?
我也觉得应当是每次处理掉最低位的obj字段指向目标锁对象的Lock Record,然后就会结束monitorexit指令。不然就会和锁重入的逻辑冲突了
大佬图片看不到
现在默认都是走模板解释器,那我想调试下字节码解释器(bytecodeInterpreter)关于monitor的代码,这个要怎么设置?
你好,你在文章中提到:“当有其他线程尝试获得锁时,就需要等到safe point时将偏向锁撤销为无锁状态或升级为轻量级/重量级锁”
请问锁升级也需要等待sofa point么?
在操作系统的书籍里,提到 “没有竞争时,futex完全在用户空间工作。”,所以传统的Synchronized为什么很慢?
1.快速用户区互斥量futex
随着并行的增加,有效的同步和锁机制对性能而言非常重要。如果等待时间短的话,自旋锁会很快。但如果等待时间长,则会浪费CPU周期。如果有很多竞争,那么阻塞此进程,并仅当锁被释放的时候让内核解除阻塞会更加有效。然而,这却带来了相反的问题:它在竞争激烈的情况下效果不错,但如果一开始只有很小的竞争,那么不停地内核切换将花销很大。更糟的是,预测锁竞争的数量并不容易。
一个引人注意的致力于结合两者优点的解决方案称作“futex”,或者“快速用户空间互斥”。futex是Linux的一个特性,它实现了基本的锁(很像互斥锁),但避免了陷入内核,除非它真的不得不这样做。因为来回切换到内核花销很大,所以这样做可观地改善了性能。一个futex包含两个部分:一个内核服务和一个用户库。内核服务提供一个等待队列,它允许多个进程在一个锁上等待。它们将不会运行,除非内核明确地对它们解除阻塞。将一个进程放到等待队列需要(代价很大的)系统调用,我们应该避免这种情况。因此,没有竞争时,futex完全在用户空间工作。特别地,这些进程共享通用的锁变量。假设锁初始值为1,即假设这意味着锁是释放状态。线程通过执行原子操作“减少并检验”来夺取锁。接下来,这个线程检查结果,看锁是否被释放。如果未处于被锁状态,那么一切顺利,我们的线程成功夺取该锁。然而,如果该锁被另一个线程持有,那么线程必须等待。这种情况下,futex库不自旋,而是使用一个系统调用把这个线程放在内核的等待队列上。可以期望的是,切换到内核的开销已是合乎情理的了,因为无论如何线程被阻塞了。当一个线程使用完该锁,它通过原子操作“增加并检验”来释放锁。并检查结果,看是否仍有进程阻塞在内核等待队列上。如果有,它会通知内核可以对等待队列里的一个或多个进程解除阻塞。如果没有锁竞争,内核则不需要参与其中。
您好!想请问一下,我看网上很多讲偏向锁的文章会把偏向锁的撤销和释放作为一个概念;还有的说偏向锁只有撤销锁的操作,没有释放锁的操作;您的文章里强调了释放和撤销的不同,请问您一下,关于这个您是怎么理解的,怎么去区分这两个操作?以及什么时候会触发这两个操作呐?期待您的回答 谢谢,还有简书里的也是我问的 您回答一个就可以啦谢谢
第二个观点是对的 , 每次来请求偏向锁的时候 , 会先比对对象头的 Thread-ID , 如果不对 ,查看当前ID线程是否存活 , 根据isAlive 来继续 决定进入轻量级锁 , 那么此处就是撤销锁, 他会遍历当前线程组 ,全部撤销 , 消耗CPU