Table of Contents
阅读此博客前必读:代码大家自己动手敲,不要太懒哈。我都给截图,不给代码!此篇博客大多都是基于实战验证理论过程,重点在于偏向级锁实战等几个实战。
一、对象头打印信息类
导入Maven依赖或者jar
<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、无锁可偏向----升级为偏向锁过程
线程进入同步代码块过程:
-
判断lock锁标志位,lock=01进入第2步操作,否则,lock=00进入轻量级锁操作,lock=10进入重量级锁操作。
-
判断biased_lock值,是否开启偏向状态,值1则为开启,则执行第3步操作,否则进入轻量级锁操作
-
判断thread ID=当前线程,等于直接进入同步代码块,并在栈中记录重入锁次数。不等于则进入第4步操作
-
直接CAS操作,如果将MarkWord的thread ID改成当前线程id,如果thread ID=0,则CAS会成功,则获得偏向锁,否则失败则进入第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 |
过程:
-
JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。
-
OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。
-
处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。
-
Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck线程的锁资源。
6、重量级锁实战
代码测试一:两个程竞争,直接升级为重量级锁。
线程1在执行时,是拿到了偏向锁,但是输出对象头信息大概需要3s中,就是同步代码需要执行时间比较长,所以当线程2来拿锁时,线程1还在执行,就造成了锁的竞争,直接升级轻量级锁,升级重量级锁的过程。
代码测试二:当线程执行完毕,不存活后,重量级锁解锁成无锁不可偏向状态(解锁后拿锁又是轻量级、重量那些操作)