内置锁探索,结合对象头分析内置锁(亲测,研究一段时间总结)

Table of Contents

一、对象头打印信息类

二、对象头介绍

三、对象头的参数说明

四、无锁分析

四、偏向锁

五、轻量级锁

六、重量级锁

七、带上一张自画图--内置锁sync升级过程图


阅读此博客前必读:代码大家自己动手敲,不要太懒哈。我都给截图,不给代码!此篇博客大多都是基于实战验证理论过程,重点在于偏向级锁实战等几个实战。

一、对象头打印信息类

导入Maven依赖或者jar

扫描二维码关注公众号,回复: 11880183 查看本文章
  <dependencies>
	<dependency>  
	    <groupId>org.openjdk.jol</groupId>  
	    <artifactId>jol-core</artifactId>  
	    <version>0.8</version>  
	</dependency> 
  </dependencies>

下载地址:http://central.maven.org/maven2/org/openjdk/jol/

我下载的是0.8版本的jol-core

使用ClassLayout类打印对象头信息(首次打印耗时比较长,大概3s)

package com.xue.sync;

import org.openjdk.jol.info.ClassLayout;

public class SyncTest {
	public static void main(String[] args) throws InterruptedException {
		A a = new A();
                a.hashCode();
		System.out.println(ClassLayout.parseInstance(a).toPrintable());
	}
	
}

class A {
	
}

一般信息如下:下面是运行结果及解释(涉及到小端存储,感兴趣自己百度一下)

二、对象头介绍

对象在内存中布局分为3块区域:对象头(Header)实例数据(Instance Data)对齐数据(Padding)

实例数据是创建对象时的成员变量的字节总数和,对齐数据是要求 对象头字节数+实例数据字节数不是8的倍数的时候,要求填充字节数让其满足是8的倍数

下面介绍对象头:

  • 普通对象头:Mark Word + Kclass Word
  • 数组对象头:Mark Word + Kclass Word + 数组长度

解释关键名称

  • Mark Word:存储运行时的数据,如hash、分代年龄age、是否为可偏向状态、锁标志位
  • Kclass Word:指向方法区的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类

在32位和64位时,对象头分布

32位

锁状态 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向锁 锁标志位
无锁 hashcode age 0 01
偏向锁 Thread Id Epoch age 1 01
轻量级锁 指向栈中锁的记录指针 00
重量级锁 指向互斥量(Monitor初始物理地址)的指针 10
GC标记 11

64位

锁状态 56bit 1bit 4bit 1bit 2bit
54bit 2bit 是否是偏向锁 锁标志位
无锁 hashcode 0 age 0 01
偏向锁 Thread Id Epoch 0 age 1 01
轻量级锁 指向栈中锁的记录指针 00
重量级锁 指向互斥量(Monitor初始物理地址)的指针 10
GC标记 11

 

三、对象头的参数说明

知道了参数,有利于后面的锁分析

参数 说明
unused 未使用位,可以说是预留位
identity_hashcode 对象标识hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中
age 分代年龄,固定4位 
biased_lock 是否开启偏向状态(0--关闭,1--开启),固定1位
lock 锁标志位(01--无锁或者偏向锁,00--轻量级锁,10--重量级锁),固定2位
thread 持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。 偏向锁才有,固定54位
epoch 偏向时间戳。偏向锁才有,固定占2位
ptr_to_lock_record 指向栈中锁记录的指针。轻量级锁才有,固定62位
ptr_to_heavyweight_monitor 指向线程Monitor的指针。重量级锁才有,固定62位

四种锁对应的的参数及位数(64位下)

无锁 | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |
偏向锁 | thread ID:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 |
轻量级锁 | ptr_to_lock_record:62 | lock:2 |
重量级锁 | ptr_to_heavyweight_monitor:62 | lock:2 | 

 

四、无锁分析

无锁不可偏向状态(001)对象头状态如下(其实hashCode那个为0,只有计算调用过hashCode才会记录到对象头中

 

无锁可偏向状态(101)对象头状态如下(这是一种特效的无锁状态,thread和epoch都为0

 

 

无锁不可偏向状态和无锁可偏向状态:

1、jdk1.6及以后默认是采用延迟加载偏向锁,这个延迟时间大概是4s,当JVM开启4s内创建的对象默认都是都是无锁不可偏向状态(001)

例如:新建一个对象,运行测试程序,此时可以保证在JVM开启4s之内,验证在4秒内是无锁不可偏向的

2、延迟时间是4s,JVM启动4秒之后创建的对象将开启偏向状态,当JVM开启4s内创建的对象默认都是都是无锁可偏向状态(101)

例如:如下列运行测试程序及结果,主线程睡眠4.5s,说明JVM启动超过了时间4s,验证在4秒后是无锁可偏向的,这是一种特殊的无锁状态,因为它的线程id为0。说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了(epoch是无效的,因为偏向锁可重偏向开启则导致其无效)

疑问:对于无锁怎么转为偏向锁?

如果是jdk1.6及之后默认开启的话,答案1:JVM开启4s前创建的对象是无锁状态的,此时这些对象只可以升级为轻量级锁和重量级锁(不能转为偏向锁)答案2:4s之后创建的对象默认是无锁可偏向的,可以理解成特殊的无锁状态,这个特殊的无锁状态只可以转为偏向锁,并且只有获得了偏向锁后,进行撤销偏向锁或者直接升级为轻量级锁,后面还可以升级为重量级锁。

答案结论:真正来说从无锁转为偏向锁是不可能实现的,但是从特殊的无锁状态转为偏向锁就可以实现

 

四、偏向锁

1、偏向锁作用:就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

2、配置偏向锁参数

启用参数: -XX:+UseBiasedLocking
关闭延迟(默认大概4s开启): -XX:BiasedLockingStartupDelay=0 
禁用参数: -XX:-UseBiasedLocking

3、jdk默认配置说明:偏向锁在jdk1.6及以后为默认开启,默认是采用延迟加载偏向锁,这个延迟时间大概是4s,当JVM开启4s内创建的对象默认都是都是无锁不可偏向状态(001)以及4s后创建的对象是无锁可偏向(101)两种状态在无锁时已经介绍。

4、对象调用hashCode方法后,将会进行撤销偏向锁操作,变成无锁不可偏向状态,并且将hashCode写入对象头中,之后也不能使用偏向锁。

5、无锁可偏向----升级为偏向锁过程

线程进入同步代码块过程:

  1. 判断lock锁标志位,lock=01进入第2步操作,否则,lock=00进入轻量级锁操作,lock=10进入重量级锁操作。

  2. 判断biased_lock值,是否开启偏向状态,值1则为开启,则执行第3步操作,否则进入轻量级锁操作

  3. 判断thread ID=当前线程,等于直接进入同步代码块,并在栈中记录重入锁次数。不等于则进入第4步操作

  4. 直接CAS操作,如果将MarkWord的thread ID改成当前线程id,如果thread ID=0,则CAS会成功,则获得偏向锁,否则失败则进入第5步操作

  5. 说明偏向锁被已经被偏向,则如下表四种可能:

1、2可能在轻量级实战演示,3可能在偏向锁下面实战演示,4可能操作演示不了,偏向锁默认开启

1、偏向的线程存活,不在执行同步代码 则升级为轻量级锁
2、偏向的线程存活,在执行同步代码 将会直接升级成轻量级锁再自旋在升级成重量级锁
3、偏向的线程不存活,开启了重偏向 将会将对象头设置成无锁可偏向的状态,然后重偏向线程,拿到偏向锁
4、偏向的线程不存活,未开启重偏向 将会进行撤销偏向锁操作,进入轻量级锁操作

注:不存活的意思是线程生命周期结束了。其实默认是可重偏向的,可重偏向需要把thread ID置为0,然后升级为偏向锁。

 

第5步操作可能比较难理解,下面给个流程图(默认是可重偏向的,可重偏向判断省略掉了)

6、偏向级锁实战

说明:jdk1.6是默认开启了偏向锁的,延迟时间是4s,代码让主线程Thread休眠4.5秒即可使创建的对象为无锁可偏向状态

代码测试一:线程拿到偏向锁(拿锁过程:CAS将thread ID=0置换成当前线程

 

代码测试二:不让主线程休眠,创建的对象是无锁不可偏向的,不能升级为偏向锁,只能升级成轻量级锁

 

代码测试三:对象调用hashCode方法后,将会进行撤销偏向锁操作,变成无锁不可偏向状态,之后也不能使用偏向锁。调用hashCode后将撤销偏向锁,并将hashCode值存进对象头。(图片二进制我写错了,正确01010010)

小端存储,调用hashCode后写入对象头

 

代码测试四3、偏向的线程不存活,开启了重偏向,将会将对象头设置成无锁可偏向的状态,然后重偏向线程,拿到偏向锁。代码说明:线程1启动拿到偏向锁,给主线程个睡眠时间,等线程1结束了后再启动线程2,线程2拿的是可重偏向,就是偏向锁重新偏向了线程2。测试上述可能

 

 

五、轻量级锁

1、轻量级锁的作用:本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

2、jdk1.6之中加入的新型锁机制。JVM启动4s前,创建的对象都是无锁不可偏向状态,此时第一个涉及同步状态的都是轻量级锁。之前也可能是重量级锁

3、无锁不可偏向----轻量级锁过程

JVM启动4s前创建的对象为无锁不可偏向状态,当第一个线程拿锁时,就直接升级为轻量级锁。

偏向锁有竞争也会升级成轻量级锁,解锁后也就是无锁不可偏向状态。

轻量级锁竞争会升级成重量级锁,解锁后还是无锁不可偏向状态。

以上三种状态最后的都是无锁不可偏向状态,此后有线程来将进行以下操作,就是轻量级锁拿锁操作:

1、判断是01状态,标志位是0不可偏向,进入步骤2

2、在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用来存储锁对象目前MarkWord的拷贝,进入3

3、CAS操作尝试将对象的MarkWord更新为指向Lock Record的指针,成功则进入4,失败进入5

4、这个线程获取轻量级锁成功,将MarkWord锁标志位改成00。

5、检查对象的MarkWord是否指向当前线程的栈帧,是的话直接进入同步代码,表名是可重入锁,否则进入6

6、线程已经被其它线程抢占,要膨胀为重量级锁。

4、偏向锁----轻量级锁过程

  • 偏向的线程存活,不在执行同步代码

  • 偏向的线程存活,在执行同步代码

5、轻量级锁解锁过程

当拿到轻量级锁的线程执行完毕,不存活的时候就会执行解锁过程

6、轻量级锁实战

代码测试一:JVM启动4s前创建的对象为无锁不可偏向状态,当第一个线程拿锁时,就直接升级为轻量级锁

 

代码测试二:偏向的线程1存活,不在执行同步代码,线程2进来拿锁直接升级为轻量级锁

 

代码测试三:偏向的线程1存活,且在执行同步代码,线程2进来拿锁竞争升级成轻量锁升级重量级锁

 

代码测试四:当拿到轻量级锁的线程2执行完毕,不存活的时候就会执行解锁过程

 

六、重量级锁

1、重量级锁加锁和释放锁需要通过调用操作系统来实现互斥同步操作。这是一种耗性能的操作。

2、每个对象都有Monitor类,在同步代码块前后分别插入monitorenter/monitorexit两条字节码指令来控制加解锁。

3、深入JVM虚拟机在391页说道:根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁住,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

4、对于Monitor解释

5、深入Monitor(这里解释搬了原文 原文链接:https://blog.csdn.net/zqz_zqz/article/details/70233767

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。 

Contention List 竞争队列,所有请求锁的线程首先被放在这个竞争队列中
Entry List Contention List中那些有资格成为候选资源的线程被移动到Entry List中
Wait Set 哪些调用wait方法被阻塞的线程被放置在这里
OnDeck 任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck
Owner 当前已经获取到所资源的线程被称为Owner

过程:

  1. JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

  2. OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

  3. 处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

  4. Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占  OnDeck线程的锁资源。

6、重量级锁实战

代码测试一:两个程竞争,直接升级为重量级锁。

线程1在执行时,是拿到了偏向锁,但是输出对象头信息大概需要3s中,就是同步代码需要执行时间比较长,所以当线程2来拿锁时,线程1还在执行,就造成了锁的竞争,直接升级轻量级锁,升级重量级锁的过程。

代码测试二:当线程执行完毕,不存活后,重量级锁解锁成无锁不可偏向状态(解锁后拿锁又是轻量级、重量那些操作)

七、带上一张自画图--内置锁sync升级过程图

猜你喜欢

转载自blog.csdn.net/qq_41055045/article/details/102679948