(转) Java 中锁实现原理以及锁升级过程
面试题
请描述 synchrnoized 和 ReentrantLock 的底层实现及重入的底层原理 – 百度、阿里
请描述锁的四种状态和升级过程 – 百度、阿里
CAS 的 ABA 问题如何解决 – 百度
请谈一下 AQS,为什么 AQS 的底层是 CAS + volatile – 百度
请谈一下你对 volatile 的理解 – 美团、阿里
volatile 的可见性和禁止指令重排序是如何实现的 – 美团
CAS 是什么 – 美团
请描述一下对象的创建过程 – 美团、顺丰
对象在内存中的内存布局 – 美团、顺丰
DCL 单例为什么要加 volatile – 美团
解释一下锁的四种状态 – 顺丰
Object o = new Object() 在内存中占了多少字节? – 顺丰
请描述 synchronized 和 ReentrantLock 的异同
聊聊你对 as-if-serial 和 happens-before 语义的理解 – 京东
你了解ThreadLocal吗?你知道ThreadLocal 中如何解决内存泄漏问题吗? – 京东、阿里
请描述一下锁的分类以及 JDK 中的应用 – 阿里
用户态与内核态
JDK1.0,synchronized 叫做重量级锁, 因为申请锁资源必须通过kernel, 系统调用
1 | ;hello.asm |
CAS
Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁 (无重量锁)
因为经常配合循环操作,直到完成为止,所以泛指一类操作
cas(v, a, b) ,变量v,期待值a, 修改值b
ABA问题,你的女朋友在离开你的这段儿时间经历了别的人,自旋就是你空转等待,一直等到她接纳你为止
解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号
Unsafe
AtomicInteger:
1 | public final int incrementAndGet() { |
Unsafe:
1 | public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); |
运用:
1 | package com.mashibing.jol; |
jdk8u: unsafe.cpp:
cmpxchg = compare and exchange
1 | UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) |
jdk8u: atomic_linux_x86.inline.hpp 93行
is_MP = Multi Processor
1 | inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { |
jdk8u: os.hpp is_MP()
1 | static inline bool is_MP() { |
jdk8u: atomic_linux_x86.inline.hpp
1 |
最终实现:
cmpxchg = cas修改变量值
1 | lock cmpxchg 指令 |
硬件:
lock指令在执行后面指令的时候锁定一个北桥信号
(不采用锁总线的方式)
markword
工具:JOL = Java Object Layout
1 | <dependencies> |
jdk8u: markOop.hpp
1 | // Bit-format of an object header (most significant first, big endian layout below): |
synchronized的横切面详解
- synchronized原理
- 升级过程
- 汇编实现
- vs reentrantLock的区别
java源码层级
synchronized(o)
字节码层级
monitorenter moniterexit
JVM层级(Hotspot)
1 | package com.mashibing.insidesync; |
1 | com.mashibing.insidesync.T01_Sync1$Lock object internals: |
1 | com.mashibing.insidesync.T02_Sync2$Lock object internals: |
InterpreterRuntime:: monitorenter方法
1 | IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) |
synchronizer.cpp
revoke_and_rebias
1 | void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) { |
1 | void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { |
inflate方法:膨胀为重量级锁
锁升级过程
JDK8 markword实现表:
new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁
synchronized优化的过程和markword息息相关
用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
Object o = new Object()
锁 = 0 01 无锁态
注意:如果偏向锁打开,默认是匿名偏向状态o.hashCode()
001 + hashcode1
200000001 10101101 00110100 00110110
01011001 00000000 00000000 00000000little endian big endian
00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000
默认synchronized(o)
00 -> 轻量级锁
默认情况 偏向锁有个时延,默认是4秒
why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。1
-XX:BiasedLockingStartupDelay=0
如果设定上述参数
new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101如果有线程上锁
上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
偏向锁不可重偏向 批量偏向 批量撤销如果有线程竞争
撤销偏向锁,升级轻量级锁
线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁如果竞争加剧
竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间
(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)
偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数
如果计算过对象的hashCode,则对象无法进入偏向状态!
轻量级锁重量级锁的hashCode存在与什么地方?
答案:线程栈中,轻量级锁的LR中,或是代表重量级锁的ObjectMonitor的成员中
关于epoch: (不重要)
批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。
原理以class为单位,为每个class维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
没错,我就是厕所所长
加锁,指的是锁定对象
锁升级的过程
JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低
现代版本进行了优化
无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
synchronized最底层实现
1 |
|
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T
C1 Compile Level 1 (一级优化)
C2 Compile Level 2 (二级优化)
找到m() n()方法的汇编码,会看到 lock comxchg …..指令
synchronized vs Lock (CAS)
1 | 在高争用 高耗时的环境下synchronized效率更高 |
锁消除 lock eliminate
1 | public void add(String str1,String str2){ |
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
锁粗化 lock coarsening
1 | public String test(String str){ |
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
锁降级(不重要)
https://www.zhihu.com/question/63859501
其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!
超线程
一个ALU + 两组Registers + PC
参考资料
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
volatile的用途
1.线程可见性
1 | package com.mashibing.testvolatile; |
2.防止指令重排序
问题:DCL单例需不需要加volatile?
CPU的基础知识
缓存行对齐
缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
Disruptor1
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
40package com.mashibing.juc.c_028_FalseSharing;
public class T02_CacheLinePadding {
private static class Padding {
public volatile long p1, p2, p3, p4, p5, p6, p7; //
}
private static class T extends Padding {
public volatile long x = 0L;
}
public static T[] arr = new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[0].x = i;
}
});
Thread t2 = new Thread(()->{
for (long i = 0; i < 1000_0000L; i++) {
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime() - start)/100_0000);
}
}MESI
伪共享
合并写
CPU内部的4个字节的Buffer1
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
62package com.mashibing.juc.c_029_WriteCombining;
public final class WriteCombining {
private static final int ITERATIONS = Integer.MAX_VALUE;
private static final int ITEMS = 1 << 24;
private static final int MASK = ITEMS - 1;
private static final byte[] arrayA = new byte[ITEMS];
private static final byte[] arrayB = new byte[ITEMS];
private static final byte[] arrayC = new byte[ITEMS];
private static final byte[] arrayD = new byte[ITEMS];
private static final byte[] arrayE = new byte[ITEMS];
private static final byte[] arrayF = new byte[ITEMS];
public static void main(final String[] args) {
for (int i = 1; i <= 3; i++) {
System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
System.out.println(i + " SplitLoop duration (ns) = " + runCaseTwo());
}
}
public static long runCaseOne() {
long start = System.nanoTime();
int i = ITERATIONS;
while (--i != 0) {
int slot = i & MASK;
byte b = (byte) i;
arrayA[slot] = b;
arrayB[slot] = b;
arrayC[slot] = b;
arrayD[slot] = b;
arrayE[slot] = b;
arrayF[slot] = b;
}
return System.nanoTime() - start;
}
public static long runCaseTwo() {
long start = System.nanoTime();
int i = ITERATIONS;
while (--i != 0) {
int slot = i & MASK;
byte b = (byte) i;
arrayA[slot] = b;
arrayB[slot] = b;
arrayC[slot] = b;
}
i = ITERATIONS;
while (--i != 0) {
int slot = i & MASK;
byte b = (byte) i;
arrayD[slot] = b;
arrayE[slot] = b;
arrayF[slot] = b;
}
return System.nanoTime() - start;
}
}指令重排序
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
48package com.mashibing.jvm.c3_jmm;
public class T04_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
//shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
//System.out.println(result);
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
系统底层如何实现数据一致性
- MESI如果能解决,就使用MESI
- 如果不能,就锁总线
系统底层如何保证有序性
- 内存屏障sfence mfence lfence等系统原语
- 锁总线
volatile如何解决指令重排序
1: volatile i
2: ACC_VOLATILE
3: JVM的内存屏障
屏障两边的指令不可以重排!保障有序!
4:hotspot实现
bytecodeinterpreter.cpp
1 | int field_offset = cache->f2_as_index(); |
orderaccess_linux_x86.inline.hpp
1 | inline void OrderAccess::fence() { |
总结
用户空间锁 VS 重量级锁
- 偏向锁、自旋锁都是用户空间完成;
- 重量级锁是需要向内核申请。
为何会有偏向锁?
- 多数 sychronized 方法,在很多情况下,只有一个线程在运行;
- 例如:StringBuffer 中的一些 sync 方法,Vector 中的一些 sync 方法;
偏向锁的实现
- 把自己的线程 ID 设置到 markword;
自旋锁
- 有人竞争(自旋锁,轻量级锁,无锁);
- 每个线程在线程栈中生成 LockRecord,用 CAS 方式尝试把自己的指针更新到 markword;
- 每一次锁重入,都会有一个 LockRecord;
偏向锁是否一定提高效率?
- 不一定
- 在明确知道多个线程强烈竞争的时候,系统会把大量资源消耗在撤销上。