synchronized使用方式

修饰类对象

修饰对象

修饰静态成员方法

修饰成员方法

总结

java的synchronized关键字是跟对象一起配合使用的,表示给这个对象加锁,加锁对象作用的代码块只能获取锁对象的线程使用,其他线程运行到该代码块,会尝试获取该对象的锁,如果该对象已经被其他线程加锁了,其他线程只能阻塞.当获取锁对象的线程执行完代码块后,它会释放锁对象,然后唤醒在获取该锁对象的所有线程,他们会再竞争获取对象锁资源.获取到对象锁资源的线程会执行代码块,没有获取到对象锁资源的会继续阻塞,等待获取对象锁资源的线程执行完代码块将其再次唤醒.

synchronized修饰后对应的字节码

1.在代码块里面修饰对应的就是monitorenter和monitorexit分别为获取对象锁和释放对象锁,如图:

2.在方法上修饰对应的就是访问修饰符,如图:

Access flags对照表见《class文件结构》第8点方法,方法的静态修饰符static为0x0020

JVM对synchronized的优化

java的Hotspot虚拟机的对象头主要包括两部分:Mark Word(标记字段)、Klass Pointer(类型指针)

32位系统中:

Mark Word = 4 bytes = 32 bits,对象头 = 8 bytes = 64 bits;

64位系统中:

Mark Word = 8 bytes = 64 bits ,对象头 = 16 bytes = 128bits;

Mark Word最后3位或最后两位对应分别对应锁的状态

正常->(001)偏向锁(101)->轻量级锁(00)->重量级锁(10)

java对象头结构

32位结构

锁状态 25bit(对象的hashcode) 4bit(对象分代年龄) 1bit(是否是偏向锁) 2bit(锁标志位)
23bit(线程ID) 2bit(Epoch)
无锁状态(Normal) 对象hashcode、对象分代年龄 01
轻量级锁(Lightweight Locked) 指向锁记录(Lock Record)的地址 00
重量级锁(Heavyweight Locked) 指向重量级锁(Monitor)的地址 10
GC标记(Marked for GC) 11
偏向锁(Biased) 线程ID、Epoch、对象分代年龄、偏向锁标志 01

64位结构

锁状态 25bit(未使用的) 31(哈希值) 1bit(未使用的) 4bit(对象分代年龄) 1bit(是否是偏向锁) 2bit(锁标志位)
54bit(线程ID) 2bit(Epoch)
无锁状态(Normal) 对象hashcode、对象分代年龄 01
轻量级锁(Lightweight Locked) 指向锁记录(Lock Record)的地址 00
重量级锁(Heavyweight Locked) 指向重量级锁(Monitor)的地址 10
GC标记(Marked for GC) 11
偏向锁(Biased) 线程ID、Epoch、对象分代年龄、偏向锁标志 01

java线程栈中Lock Record结构

Lock Record 描述
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
RcThis 表示blocked或waiting在该monitor record上的所有线程的个数;
Nest 用来实现 重入锁的计数;
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

Monitor对象(C++)实现(参考源码文件url: https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0;
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

线程对象在Monitor的流转步骤图:

jdk1.6以后对synchronized执行方式的优化

轻量级锁

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 场景:
当JVM关闭偏向锁(默认是开启的,采用-XX:-UseBiasedLocking)功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁.
## 获取锁步骤
1.判断当前对象是否处于无锁状态(hashcode、0、01),若是则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤3
2.JVM利用CAS操作尝试将对象的Mark Word更新位指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示该对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤3
3.判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀位重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态
## 释放锁
1.取出在获取轻量级锁保存在Displaced Mark Word中的数据;
2.用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行3;
3.如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程.

-- 总计:
对于轻量级锁,其性能提升的依据是"对于绝大部分的锁,在整个生命周期内都是不会存在竞争的",如果打破这个一句则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢.

流程图:

偏向锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 场景:
在没有多线程竞争的情况下,系统默认(-XX:+UseBiasedLocking)会采用加偏向锁的方式来获取锁.
## 获取锁步骤
1.检测Mark Word是否为可偏向状态,及是否为偏向锁1,锁标识位是否为01,如果不为偏向锁标识不为1,则走轻量级锁获取锁流程
2.如果为可偏向状态,则判断锁对象的线程是否为当前线程,如果是,则直接走步骤5,否则执行步骤3
3.如果锁对象的Mark Word线程id不为当前线程id,如果Mark Word线程id为空,则直接走CAS操作竞争锁,如果竞争成功,对象锁的Mark Word更新为当前线程的id,然后直接走步骤5.否则走步骤4;如果Mark Word线程id不为空,则直接走轻量级锁流程,并将重偏向的记录+1,如果默认类对象阈值累计达到了20且未超过规定间隔时间,后续将走批量重偏向流程,则直接走CAS操作竞争锁,如果竞争成功,对象锁的Mark Word更新为当前线程的id,然后直接走步骤5.否则走步骤4.
4.通过CAS竞争锁失败,证明当前存在多线程竞争的情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后阻被阻塞在安全点的线程继续往下执行同步代码块.
5.执行同步代码块
6.特别说明如果该对象类锁发生二次重偏向的累计值超过默认40且未超过规定间隔时间,后续创建锁的类对象的对象默认为正常对象后续将不为进行获取偏向锁的流程
## 释放锁步骤
1.暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态
2.撤销偏向锁,恢复到无锁状态01或轻量级锁的状态

总结:
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行.因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的.因为偏向锁只需检查偏向锁、锁标识以及ThreadId即可.
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程不会主动去释放偏向锁,需要等待其他线程来竞争.偏向锁的撤销需要等待全局安全点.(这个时间点上没有正在执行的代码)

可使用 java -XX:+PrintFlagsFinal -version |grep Biased 查看JVM相关偏向锁的默认配置,如图:

1
2
3
4
5
6
7
8
##参数解析
1.UseBiasedLocking :是否开启偏向锁设置(-XX:[+/-]UseBiasedLocking)
2.TraceBiasedLocking :是否开启偏向锁的日志跟踪(-XX:[+/-]TraceBiasedLocking)
3.BiasedLockingStartupDelay :偏向锁启动的延迟时间(-XX:BiasedLockingStartupDelay=4000(时间单位毫秒)))
4.BiasedLockingDecayTime :最近该类两次偏向锁使用的间隔时间超过该时间,重偏向锁和撤销偏向锁的阈值计数变量都将清零(-XX:BiasedLockingDecayTime=25000(时间单位毫秒)))
5.BiasedLockingBulkRebiasThreshold :批量重偏向的阈值.未到达该阈值时,出现其他线程竞争该对象时,会出现锁撤销或升级为轻量级锁;达到或者超过该阈值时,出现其他线程竞争该对象时,会出现批量重偏向(之后的对象会直接替换为竞争线程的对象id)(-XX:BiasedLockingBulkRebiasThreshold=20);
6.BiasedLockingBulkRevokeThreshold :批量撤销的阈值.未达到该阈值时,直接走上面批量重偏向的逻辑.到达该阈值时,直接走轻量级锁的逻辑(-XX:BiasedLockingBulkRevokeThreshold=40)
特别说明:3、4、5、6的作用单元是单个类的锁
1
2
3
4
5
6
7
8
9
10
打印对象头地址需要依赖openjdk的jar,pom如下
<!-- 查看内存布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>

使用下面代码做对象头的打印输出
ClassLayout.parseInstance(object).toPrintable()

测试验证

参数1验证执行结果,如图:

参数2验证执行结果:

参数3验证执行结果:

1.休眠1s的执行结果:

2.调整参数设置为1s的执行结果:

参数5验证执行结果:

阈值未调整前,t2线程加的都是轻量级锁,因为批量偏向锁的默认阈值为20,如图:

将阈值调成5(-XX:BiasedLockingBulkRebiasThreshold=5),t2线程在达到阈值时,加的还是偏向锁,直接把锁对象的线程id更新为t2的线程id,执行结果如图:

参数6验证执行结果:

阈值未调整前,主线程创建的对象还是未默认偏向锁的对象(因为阈值为40),现在测试类中二次批量重偏向只跑了11次,如图:

将阈值调成10(-XX:BiasedLockingBulkRevokeThreshold=10),累计二次重偏向达到10次时,新创建出锁对象默认就没有偏向锁了,如图

参数4验证执行结果:

该参数为默认对象锁批量重偏向和批量重撤销偏向的统计间隔时间,超过则会清零重新统计,默认阈值为25秒,每个线程的间隔时间会休眠4秒,线程3每个工作任务的间隔为0.24秒,批量重偏向的阈值为6,批量撤销偏向锁为10,所以将间隔时间调整成6秒(-XX:BiasedLockingDecayTime=6000),执行结果如图:

锁消除

1
JVM中JIT编译会默认开启锁消除的优化(-XX:+EliminateLocks),条件为如果加锁对象没有逃逸自己的方法内部,编译器会默认将加锁块进行锁消除.通过

可使用 java -XX:+PrintFlagsFinal -version |grep EliminateLocks 查看JVM相关偏向锁的默认配置,如图:

该测试需要使用JMH来进行代码块的测试,后面再补充例子说明.

锁的自旋

1
线程的阻塞和唤醒需要CPU从用户态转换为核心态,频繁的阻塞和唤醒对于CPU来说是一件负担很重的工作.为了减少短时间内的线程阻塞和唤醒,JVM让竞争获取锁失败的线程进行自旋等待(通过-XX:+UseSpinning参数进行设置).如果超过了自旋等待的次数(默认为10次,通过-XX:preBlockSpin=10参数进行设置)(备注:jdk8的这两个设置参数都已不存在),锁对象膨胀为重量级锁,线程进入EntryList进行等待.jdk1.6引入了适应性自旋锁,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.

jdk8和jdk6对应默认参数的配置情况,通过java -XX:+PrintFlagsFinal -version | findStr Spin指令进行查看,如图:

重量级锁

1
2
3
重量级锁是通过锁对象内部的一个叫做Monitor(监视器)来实现的.但是监视器本质又是依赖于底层操作系统的Mute Lock来实现的.而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对较长的时间.

Monitor对象是由C++实现的.包含两个对象_WaitSet和_EntryList,多个线程竞争对象锁的时候,未竞争到的线程会进入到_EntryList中阻塞等待,线程状态为(Blocked),如果已进入到同步代码块的线程发现执行条件不满足,它可以执行wait(等待)方法,执行后该线程会进入到_WaitSet中,同时把_Owner和_count进行清空,允许在_EntryList的线程继续去竞争获取对象锁,进行到对象锁的线程会将_Owner设置为自己线程的指针同时_count进行+1.

测试类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package per.guc.gucproject.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Test12 {

static final Object lock = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("t1 尝试获取锁");
synchronized (lock){
log.debug("t1 获取锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock){
log.debug("t1 条件未满足,进入了等待");
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("t1 被唤醒,继续执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
log.debug("t1 执行完成");
});

Thread t2 = new Thread(()->{
log.debug("t2 尝试获取锁");
synchronized (lock){
log.debug("t2 获取锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock){
log.debug("t2 再次获取锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
log.debug("t2 执行完成");
});

t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("唤醒t1线程");
synchronized (lock) {
lock.notify();
}
}
}

执行结果如下图:

参考博客:

  1. https://www.zhihu.com/question/54565422 –博主写的很细,具体细节还待理解

  2. https://blog.csdn.net/qq_31865983/article/details/105003612 –偏向锁源码解析参考博客

  3. https://juejin.cn/post/6844903928681742344 –偏向锁解析流程参考博客

  4. https://blog.csdn.net/qq_16268979/article/details/124285921 –偏向锁升级说明博客

  5. https://www.cnblogs.com/zhouwangwang/p/13763887.html –java对象头说明

  6. https://www.cnblogs.com/hooong/p/14871438.html –synchronized锁升级过程说明

  7. https://blog.csdn.net/javazejian/article/details/72828483 –synchronized实现原理

  8. https://juejin.cn/post/6844903640197513230 –synchronized实现原理

  9. https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/runtime –hotspot虚拟机源码url(重要)

  10. https://www.cnblogs.com/minikobe/p/12123065.html –synchronized底层实现monitor详解

  11. https://www.cnblogs.com/minikobe/p/12122922.html –synchronized详解(主要参考博客)