锁的竞争可以理解是markword的竞争。
一、简介
本文做作为知识点的补充,有些情况并没有进行测试。
二、markword结构图
64位虚拟机markword结构图:
三、锁的多种状态
我们一般认为锁的状态是:无锁、偏向锁、轻量级锁、重量级锁;
锁的升级流程:无锁-》偏向锁-》轻量级锁-》中重量级锁;
一些东西并非像我们了解的那样,至少我认为这个过程介绍的并不完整,并且还有坑,不知是否有一样的人和我掉入坑的小伙伴。
锁的状态应该分为: 无锁、匿名偏向锁、偏向锁、轻量级锁、重量级锁。
- 无锁: 0 01;
- 匿名偏向锁: 1 01,但是偏向锁线程指针为空;
- 偏向锁: 1 01,偏向锁线程指针指向当前持有线程;
- 轻量级锁: 00;
- 重量级锁: 10。
无锁-》偏向锁-》轻量级锁-》中重量级锁,流程中指的无锁不应该是 0 01无锁,而是匿名偏向锁,下面我们通过数据可以进行验证。
四、工具
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
JVM采用的是小端模式,需要现将其转换成大端模式,具体转换如下图所示:
可以先了解以下对象结构https://blog.csdn.net/sunboylife/article/details/114275416?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22114275416%22%2C%22source%22%3A%22sunboylife%22%7D
五、无锁和匿名偏向锁
HotSpot虚拟机默认开启偏向锁延迟BiasedLockingStartupDelay以前的默认值为4000,这将延迟锁定的使用延迟了4 s(4000 ms)
1.无锁
public class NonLockTest {
public static void main(String[] args) {
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
可知打印的是 0 01 无锁态。
2.匿名偏向锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
1 01是偏向锁的标识,后方指向线程,这种情况就是上文所说的匿名偏向锁。
当然我们也可以通过设置偏向锁延迟时间为0,不用等待5秒直接打印上图结果,效果是一样的。
-XX:BiasedLockingStartupDelay=0
问题:为什么等待5秒后结果就不一样?
答: 1中存在偏向锁延迟,当执行代码的时候,偏向锁还未开启,因此是无锁状态;2中因为等待5秒后偏向锁已经开启,因此打印的是偏向锁状态,但由于没有发生线程占用,因此没有指向任何线程,所以此时的状态是匿名偏向锁。
3 无锁和匿名偏向锁的区别
3.1 无锁和匿名偏向锁的时机不同
由2可知
3.2 无锁与匿名偏向锁升级过程存在的区别
无锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
// TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
可以看到无锁是直接升级为轻量级锁并没有进入偏向锁的状态。
匿名偏向锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
可以看到匿名偏向锁是进入偏向锁的状态,并且指向了当前线程。
总结: 无锁态不会进入偏向锁态,而是直接进入轻量级锁态;匿名偏向锁会进入偏向锁态。
3.3 无锁和匿名偏向锁状态,执行同步代码释放锁后的状态区别
无锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
// TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
未进入同步代码块前是无锁状态,进入代码块后是轻量级锁状态,最后退出同步代码块恢复到无锁状态。轻量级锁释放锁后,marworkd数据会恢复。
匿名偏向锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
进入同步代码块之前是匿名偏向锁,进入同步代码块之后是偏向锁,退出同步代码块之后仍然保留偏向锁数据。偏向锁会释放但是markword数据不会恢复:也为同一个线程多次使用节省cas次数来提高性能,因此偏向锁是不可逆的。
这里我们在拓展一下:
添加代码
new Thread(() -> { synchronized (object){ System.out.println(ClassLayout.parseInstance(object).toPrintable()); } }).start();
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
new Thread(() -> {
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}).start();
}
}
很明显子线程在使用锁的时候,主线程已经退出同步代码块并释放锁了
虽然主线程已经退出代码块了,子线程拿到锁后偏向锁还是升级为轻量级锁,并没有因为主线程释放偏向锁后子线程拿到的是偏向锁,而是拿到的是升级后的轻量级锁。
得出结论: 只要出现线程一个线程以上使用偏向锁,导致偏向锁升级为轻量级锁。
3.4 无锁与匿名偏向锁状态,同步代码块中使用hashcode的区别
无锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
// TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object.hashCode();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
无锁态-》轻量级锁态-》重量级锁态,因为当同步代码块中调用hashcode的时候锁并未释放,但存在hashcode覆盖轻量级锁信息的风险,因此进行锁的升级来解决锁和hashcode并存的问题,
问题:为什么重量级锁可以解决hashcode和锁并存的问题,而轻量级锁不可以?
答:可能有人疑惑,持有轻量级的线程不是又拷贝markword吗?直接取不就行了,是的,这样理解是没问题的,但是栈是私有内存区域,如果是多线程的情况就不安全了,因为栈随时又弹出的时候;重量级之所以可以因为重量级锁会为每个对象在堆区创建一个Monitor实例
这里的互斥量实际就是ObjectMonitor的实现 Monitor。
ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
在HotSpot虚拟机中,Monitor是基于C++的ObjectMonitor类实现的,其主要成员包括:
ObjectMonitor 的源代码
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
//多线程竞争锁进入时的单向链表
_cxq = NULL ;
FreeNext = NULL ;
//_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
- _EntryList:存放处于等待锁block状态的线程队列
- _count:约为_WaitSet 和 _EntryList 的节点数之和
- _cxq: 多个线程争抢锁,会先存入这个单向链表
- _recursions: 记录重入次数
- _header:用来保存锁对象的mark word的值。因为object里面已经不保存mark word的原来的值了,保存的是ObjectMonitor对象的地址信息。当所有线程都完成了之后,需要销毁掉ObjectMonitor的时候需要将原有的header里面的值重新复制到mark word中来。
- _object: 指向的是对象的地址信息,方便通过ObjectMonitor来访问对应的锁对象。
堆区正常都是共享内存块,因此多线程之间解决hashcode和持锁线程并存的问题,直接升级重量级锁。
匿名偏向锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
object.hashCode();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
}
和无锁的情况是一样的,相同解释。
因此匿名偏向锁更像平常说的无锁态,不管怎么样,根据每个人的习惯记忆吧。
3.5 关闭偏向锁二者会有什么区别
关闭偏向锁
-XX:-UseBiasedLocking
无锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
//TimeUnit.SECONDS.sleep(6);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
偏向锁是否开启对无锁状态并没有什么影响
匿名偏向锁
因为偏向锁被关闭了,肯定也就是无锁状态了,因此和无锁的结果是一样的。
六、无锁
可通过关闭偏向锁、释放轻量级锁获得无锁状态。
七、匿名偏向锁
可通过开启偏向锁获得。
八、偏向锁
1.从3.3中可知,偏向锁不可逆;
2.无法从通过无锁态进行升级到偏向锁,只能通过匿名偏向锁进行升级成为偏向锁。
九、轻量级锁
1.从3.3中可知,轻量级锁释放后会回复到无锁状态,轻量级锁是可逆的;
2.从3.3中可知,偏向锁一个线程以上访问markword将从偏向锁升级为轻量级锁。
十、重量级锁
1.从3.4中可知,hashcode和锁记录竞争更新markword发生并存冲突时,会导致升级为重量级锁;
2.重量级锁释放后markword仍一直指向堆区Monitor实例,因此重量级锁是不可逆的;
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
User object = new User();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
object.hashCode();
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
System.out.println(ClassLayout.parseInstance(object).toPrintable());
}
}
多线程测试
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
User object = new User();
System.out.println("主线程" + ClassLayout.parseInstance(object).toPrintable());
synchronized (object){
object.hashCode();
System.out.println("主线程"+ ClassLayout.parseInstance(object).toPrintable());
}
System.out.println("主线程" + ClassLayout.parseInstance(object).toPrintable());
new Thread(() ->{
synchronized (object){
System.out.println("子线程" + ClassLayout.parseInstance(object).toPrintable());
}
System.out.println("子线程" + ClassLayout.parseInstance(object).toPrintable());
}).start();
}
}
主线程和子线程打印markword信号量指针相同。
3. 轻量级锁竞争失败升级为重量级锁
public class NonLockTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
User object = new User();
System.out.println("主线程" + ClassLayout.parseInstance(object).toPrintable());
//同步代码块后升级为偏向锁
synchronized (object){
System.out.println("主线程"+ ClassLayout.parseInstance(object).toPrintable());
}
new Thread(() ->{
//step3, step3快于step2执行
synchronized (object){
//此时偏向锁升级为轻量级锁
System.out.println("主线程竞争前,子线程状态" + ClassLayout.parseInstance(object).toPrintable());
try {
//暂停等待主线程执行step2,但不释放锁资源
TimeUnit.SECONDS.sleep(10);
//由于主线程长时间未竞争成功,导致锁升级,因此此处应该是重量级锁状态
System.out.println("主线程竞争后,子线程状态" + ClassLayout.parseInstance(object).toPrintable());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
TimeUnit.SECONDS.sleep(2);
//step2
synchronized (object){
//此时由于轻量级锁升级,打印重量级锁
System.out.println("主线程竞争锁" + ClassLayout.parseInstance(object).toPrintable());
}
}
}