锁相关原理
Monitor
对象头
一个java 对象
- 对象头
- mark Word 标记字段
- klass Word 类型指针(指向class类型)
- 对象body
-
成员变量等
-
- 对象头示意图
名称解释
hashcode:对象的hashcode值,对象创建时默认为0,只有当需要使用到hashcode时才会计算 (这也属于一种懒加载的设计理念)
age: 对象年龄,主要跟虚拟机回收有关
biased_lock : 可偏向标识 0 不可偏向, 1 可偏向 thread : 线程ID ptr_to_lock_record : 轻量级锁记录地址 ptr_to_heavyweight_monitor: 重量级锁monitor头
Monitor(锁)
Monitor被翻译为 监视器 或 管程
如果使用 synchronized 给对象上锁 (重量级)之后,该对象就会关联一个Monitor对象 ,该对象头的mark Word就会被设置指向 Monitor对象的指针。
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor的所有者 Owner设置为 Thread-2 ,
Monitor中只能有一个 Owner - 在Thread-2 上锁过程中,如果Thread-3 ,Thread-4,Thread-5 也来执行 synchronized(obj), 就会进入 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的(不会按照先到先得的方式竞争)
- 图中 WaitSet 中的 Thread-0 ,Thread-1 是之前获得过锁,但条件不满足,进入 WAITING状态的线程
(比如手动调wait 进入等待的线程)
synchronized 锁
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者的透明的,即语法仍然是 synchronized
- 假设有两个方法同步块,利用同一个对象加锁
public void method1(){
synchronized(obj) {
//同步块A
method2();
}
}
public void method2(){
synchronized(obj) {
//同步块B
}
}
- 加锁过程
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换Object的 Mark Word,将Mark Word的值存入锁记录
- 如果cas替换成功,对象头中存储了
锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下
- 如果cas失败,有两种情况
- 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数
- 当退出 synchronized代码块(解锁时) 如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用 cas将 Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
public void method1(){
synchronized(obj) {
//同步块A
method2();
}
}
- 加锁过程
-
当Thread - 1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
这时Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object对象申请
Monitor锁,让Object指向重量级锁地址 - 然后自己进入Monitor的 EntryList Blocked
- 即为 Object对象申请
-
当Thread-0 退出同步块解锁时,使用cas将 Mark Word的值恢复给对象头
- 如果失败,则进入重量级锁解锁流程
按照 Monitor地址找到Monitor对象,设置Owner为 null ,唤醒EntryList中的BLOCKED线程
-
自旋优化
重量级锁竞争的时候,还可以通过自旋来进行优化。
按照synchronized竞争锁的流程,如果线程竞争失败,则进入BLOCKED状态,等待解锁后自动唤醒,这就有个线程上下文切换的消耗。
因此增加了自旋优化,也就是竞争失败的线程先不阻塞,而是通过自旋一段时间(自旋简单理解为占着CPU空跑)或一定次数,如果当前线程自旋成功(即这时候持有锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋会占用cpu时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
在java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能 java7 之后不能控制是否开启自旋
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作(添加一条锁记录,其中锁记录 null,参照上面的轻量级锁加锁过程)
java6中引入了偏向锁来做进一步优化:只有第一次使用cas将线程ID设置到对象的 Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有
public void method1(){
synchronized(obj) {
//同步块A
method2();
}
}
public void method2(){
synchronized(obj) {
//同步块B
method3();
}
}
public void method3(){
synchronized(obj) {
//同步块B
}
}
- 两种加锁方式比较
- 加轻量级锁的过程
- 加偏向锁的过程
- 加轻量级锁的过程
注意
偏向锁默认是开启的,但默认是有延迟的,不会在程序启动时立即生效,如果想避免延迟,需要添加参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
在jvm中 -XX表示关闭 +XX表示开启某功能
- 借助第三方工具查看对象头信息
<!-- 对象头查询工具类-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
<scope>provided</scope>
</dependency>
- 测试方法
public static void main(String[] args) {
Room obj = new Room();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(()-> {
log.debug("synchronized之前..");
log.debug(classLayout.toPrintableSimple());
synchronized (obj) {
log.debug("synchronized中..");
log.debug(classLayout.toPrintableSimple());
}
log.debug("synchronized之后..");
log.debug(classLayout.toPrintableSimple());
},"t1").start();
}
其中 toPrintableSimple 需要手动实现
public String toPrintableSimple() {
return toPrintableSimple(classData.instance());
}
public String toPrintableSimple(Object instance) {
StringBuilder sb = new StringBuilder();
String markStr = "";
String remind = "";
int markSize = model.markHeaderSize();
int markOffset = 0;
if (instance != null) {
VirtualMachine vm = VM.current();
if (markSize == 8) {
long mark = vm.getLong(instance, markOffset);
markStr = Long.toBinaryString(mark);
remind = parseMarkWord(mark);
} else if (markSize == 4) {
int mark = vm.getInt(instance, markOffset);
markStr = Integer.toBinaryString(mark);
remind = parseMarkWord(mark);
}
}
// 高位补0
int i = 1;
for (; i <= 8 * markSize - markStr.length(); i++) {
sb.append('0');
if (i % 8 == 0) {
sb.append(" ");
}
}
for (; i <= 8 * markSize; i++) {
sb.append(markStr.charAt(i - (8 * markSize - markStr.length()) - 1));
if (i % 8 == 0) {
sb.append(" ");
}
}
sb.append(remind);
return sb.toString();
}
- 测试结果
- 对象头
- 程序刚启动,未加偏向锁,中途加轻量级锁,解锁后回到正常状态
- 程序启动后,等4s
- 对象头
public static void main(String[] args) throws InterruptedException {
//等待4s
Room one = new Room();
ClassLayout test = ClassLayout.parseInstance(one);
log.debug("程序刚启动的..");
log.debug(test.toPrintableSimple());
TimeUnit.SECONDS.sleep(4);
Room obj = new Room();
ClassLayout classLayout = ClassLayout.parseInstance(obj);
new Thread(()-> {
log.debug("synchronized之前..");
log.debug(classLayout.toPrintableSimple());
synchronized (obj) {
log.debug("synchronized中..");
log.debug(classLayout.toPrintableSimple());
}
log.debug("synchronized之后..");
log.debug(classLayout.toPrintableSimple());
},"t1").start();
}
可以看到最开始的未加锁 001
4秒后是 101 偏向锁
解锁后,还是偏向锁,且线程Id还是保留在对象头里
偏向锁撤销
- 调用对象的 hashCode 方法会撤销偏向锁
- 有其他线程竞争对象时,会撤销偏向锁,升级为轻量级锁
- 调用 wait/notify 也会撤销偏向锁
- 因为只有在synchronized中才能使用wait/notify,也就是本身就是重量级锁了,所以会升级
批量重偏向
- 如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程 T1的对象仍有机会重新偏向T2 ,重偏向会重置对象的Thread Id
- 当撤销偏向锁阈值超过 20次后,jvm会这样觉得,我是不是偏向错了呢? 于是会在给这些对象加锁时重新偏向至加锁线程
例子
- 在Thread 0 中对30个对象加锁,这时都是偏向锁
- 在Thread-1 中对上述30个对象再加锁,这时候就会产生锁升级,由偏向锁升级为轻量级锁
- 一直升级了19个对象的锁之后,jvm就会进行批量重偏向,对于剩下的对象不会再做锁升级,而是改为偏向Thread-1 的偏向锁
- 最终结果就是30个对象中,前19个是轻量级锁,第20个对象开始都是偏向锁-偏向Thread-1
批量撤销
批量撤销要接上面批量重定向来讲
- 如果偏向撤销的数量达到了39个,这时候jvm会认为这个类的对象不应该被加偏向锁,第40个对象开始都是不可加偏向锁,
从此这个类的所有对象都是不可偏向(后续新增的对象也是如此)
锁粗化
static Object obj = new Object();
@Test
public void test_锁粗化() {
for (int i=0;i<10;i++){
synchronized(obj) {
i++;
}
}
}
JIT编译器会认为这块没必要如此频繁的加锁/解锁,于是干脆把加锁动作放到循环外面
static Object obj = new Object();
@Test
public void test_锁粗化() {
syhchronized(obj){
for (int i=0;i<10;i++){
i++;
}
}
}
锁消除
static Object obj = new Object();
@Test
public void test_锁消除() {
int i = 0;
synchronized(obj) {
i++;
}
}
JIT编译器会认为这块没有任何竞争,加锁没有任何意义,因此会优化掉加锁动作,等同于
static Object obj = new Object();
@Test
public void test_锁消除() {
int i = 0;
i++;
}
有问题的锁
死锁
- 最简单的场景就是2个线程互相持有了对方需要的锁,双方都无法获得需要的锁
- 查询办法
- 1.1 使用jps 查询java进程
- 1.2 使用 jstack 即可看到详情的死锁信息
- 2.1 借助 jconsole
活锁
活锁是两个线程互相改变对方的结束条件导致双方都无法正常结束
饥饿
饥饿是某个线程始终无法获得锁
- 使用notify 随机唤醒waitSet中一个线程时,一直没唤醒到某线程
- 在EntryList中重新竞争的时候某线程一直获得不到锁
wait原理
- 原理介绍
- Owner 线程发现条件不满足,调用 wait方法,进入 waitSet 变为 waiting状态
- BLOCKED 和 waiting的线程都处于阻塞状态,不占用cpu时间片
- blocked 线程会在 Owner线程释放锁时唤醒
- waiting 线程会在Owner线程调用 notify 或 notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,
仍需进入EntryList重新竞争