Java 中的锁 synchronized
Java juc 锁 About 21,632 words锁的状态
无锁状态、偏向锁、轻量级锁、重量级锁。
偏向锁加锁过程
- 访问
Mark Word
中偏向锁的标识是否设置成1
,锁标志位是否为01
,确认为可偏向状态。 - 如果为可偏向状态,则判断
Mark Word
中记录的线程ID
是否指向当前线程,如果是,进入步骤(5
),否则进入步骤(3
)。 - 如果线程
ID
并未指向当前线程,则通过CAS
操作竞争锁。如果竞争成功,则将Mark Word
中线程ID
设置为当前线程ID,然后执行(5
);如果竞争失败,执行(4
)。 - 如果
CAS
获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint
)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。 - 执行同步代码。
偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为001
)或轻量级锁(标志位为000
)的状态。
轻量级锁加锁过程
- 代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为
001
状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record
)的空间,用于存储锁对象目前的Mark Word
的拷贝,官方称之为Displaced Mark Word
。 - 拷贝对象头中的
Mark Word
复制到锁记录中。 - 拷贝成功后,虚拟机将使用
CAS
操作尝试将对象的Mark Word
更新为指向Lock Record
的指针,并将Lock record
里的owner
指针指向object mark word
。如果更新成功,则执行步骤(3
),否则执行步骤(4
)。 - 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象
Mark Word
的锁标志位设置为000
,即表示此对象处于轻量级锁定状态。 - 如果这个更新操作失败了,虚拟机首先会检查对象的
Mark Word
是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要升级为重量级锁,锁标志的状态值变为010
,Mark Word
中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁的释放
- 通过
CAS
操作尝试把线程中复制的Displaced Mark Word
对象替换当前的Mark Word
。 - 如果替换成功,整个同步过程就完成了。
- 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已升级),那就要在释放锁的同时,唤醒被挂起的线程。
Mark Word 示意图
锁标志位
根据Mark Word
示意图可知,最后三位确定锁的状态:
- 无锁状态:
001
- 偏向锁:
101
- 轻量级锁:
000
- 重量级锁:
010
jol
使用OpenJDK
提供的jol
分析对象的布局,包括:Mark Word
,类型压缩、实例数据、内存填充。
Object lock1 = new Object();
System.out.println("lock1 hashcodeHex#" + Integer.toHexString(lock1.hashCode()));
System.out.println(ClassLayout.parseInstance(lock1).toPrintable());
输出:
lock1 hashcodeHex#1540e19d
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意
由于大多数CPU
都是小端存储,所以存储后打印顺序是反向的。
大端模式(Big-endian
):是指数据的高字节,保存在内存的低地址中,而数据的低字节,保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理,地址由小向大增加,而数据从高位往低位放。
小端模式(Little-endian
):是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内在的低地址中,这样的存储模式将地址的高低和数据位权有效结合起来,高地址部分权值高,低地址部分权值低。
十六进制应该是这样的:1540e19d
,二进制应该是这样的:(后8
位为分代年龄、锁标志位等其他信息)
00000000 00000000 00000000 00010101 01000000 11100001 10011101 00000001
但是,实际十六进制哈希值是这样的:9de14015
,二进制实际是这样的:
00000001 10011101 11100001 01000000 00010101 00000000 00000000 00000000
对象布局
示意图
说明
普通对象在开启类指针压缩后对象头:前8
为Mark Word
信息,后4
位为类指针。
数组对象在开启类指针压缩后对象头:前8
为Mark Word
信息,中间4
为类指针,后4
位为数组长度。
JDK1.8
后默认开启了类指针压缩,可使用-XX:-UseCompressedClassPointers
关闭。
示例代码
public static void classLayout() {
Object obj = new Object();
System.out.println("obj hashcodeHex#" + Integer.toHexString(obj.hashCode()));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
System.out.println("----------------------------------------------------------");
int[] arr = new int[10];
System.out.println("arr hashcodeHex#" + Integer.toHexString(Arrays.hashCode(arr)));
System.out.println(ClassLayout.parseInstance(arr).toPrintable());
}
开启类指针压缩
JDK1.8
默认开启了类指针压缩
obj hashcodeHex#1540e19d
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
----------------------------------------------------------
arr hashcodeHex#94e4b2c1
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 20 (01101101 00000001 00000000 00100000) (536871277)
12 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
16 40 int [I.<elements> N/A
Instance size: 56 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
关闭类指针压缩
启动时添加:-XX:-UseCompressedClassPointers
obj hashcodeHex#1540e19d
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) 00 1c a8 17 (00000000 00011100 10101000 00010111) (396893184)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
----------------------------------------------------------
arr hashcodeHex#94e4b2c1
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 68 0b a8 17 (01101000 00001011 10101000 00010111) (396888936)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 4 (object header) 0a 00 00 00 (00001010 00000000 00000000 00000000) (10)
20 4 (alignment/padding gap)
24 40 int [I.<elements> N/A
Instance size: 64 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
偏向锁
适应只有一个线程执行的场景,如出现了两个线程不管是交替执行还是同时竞争,偏向锁都将升级为重量级锁。
在同步代码块(synchronized
代码块)外调用hashCode
方法后(重写后的hashCode
方法不影响),hashCode
会写进头信息中,偏向锁也升级为轻量级锁。
在同步代码块(synchronized
代码块)内调用hashCode
方法后(重写后的hashCode
方法不影响),hashCode
会写进头信息中,偏向锁也升级为重量级锁。
偏向锁的演示需等程序启动5
秒后,或者添加jvm
参数:-XX:BiasedLockingStartupDelay=0
,表示偏向锁启动延迟时间为0
秒。
JDK1.6
后JVM
默认开启偏向锁,如需禁用偏向锁可添加参数:-XX:-UseBiasedLocking
。
偏向锁相关问题
问:为什么调用hashCode
方法后偏向锁会升级?
答:哈希值默认是0
调用hashCode
方法后会生成一个31
位长度的值,而偏向锁需要储存54
位长度的线程ID
的值,故没有更多空间储存31
位长度的哈希值,只能让偏向锁升级。
问:轻量级锁和重量级锁调用hashCode
方法后,31
位长度的哈希值储存在什么位置?
答:轻量级锁调用hashCode
方法后将31
位长度的哈希值储存在线程栈的Lock Record
中,重量级锁调用hashCode
方法后将31
位长度的哈希值储存在Monitor
对象中。
示例代码
被注释掉的线程A'
打开注释后,两个线程争夺lock1
锁,偏向锁也就会升级为重量锁。
public static void biasedLock() throws InterruptedException {
// 等待偏向锁启动
TimeUnit.SECONDS.sleep(5);
System.out.println("开始演示...");
Object lock1 = new Object() {
@Override
public int hashCode() {
return 123456;
}
};
Object lock2 = new Object();
Object lock3 = new Object();
new Thread(() -> {
synchronized (lock1) {
System.out.println("lock1 Thread id#" + Thread.currentThread().getId() + ", hashCode#" + Thread.currentThread().hashCode() + ", hashCodeHex#" + Integer.toHexString(Thread.currentThread().hashCode()));
System.out.println(ClassLayout.parseInstance(lock1).toPrintable() + "\n##########################################");
}
}, "A").start();
/*new Thread(() -> {
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock1) {
System.out.println("lock1' Thread id#" + Thread.currentThread().getId() + ", hashCode#" + Thread.currentThread().hashCode() + ", hashCodeHex#" + Integer.toHexString(Thread.currentThread().hashCode()));
System.out.println(ClassLayout.parseInstance(lock1).toPrintable() + "\n##########################################");
}
}, "A'").start();*/
new Thread(() -> {
synchronized (lock2) {
System.out.println("lock2 hashcodeHex#" + Integer.toHexString(lock2.hashCode()));
System.out.println(ClassLayout.parseInstance(lock2).toPrintable() + "\n------------------------------------------");
}
}, "B").start();
System.out.println("lock3 hashcodeHex#" + Integer.toHexString(lock3.hashCode()));
new Thread(() -> {
synchronized (lock3) {
System.out.println(ClassLayout.parseInstance(lock3).toPrintable() + "\n******************************************");
}
}, "C").start();
}
头信息
可以看到:
- 线程
A
没有其他线程竞争锁lock1
,锁的标志位为101
(偏向锁) - 线程
B
在同步代码块内调用了hashCode
,偏向锁升级为重量级锁,锁的标志位为010
(重量级锁) - 线程
C
在同步代码块外调用了hashCode
,偏向锁升级为轻量级锁,锁的标志位为000
(轻量级锁) - 若打开注释掉的线程
A'
,则偏向锁lock1
将升级为重量级锁
开始演示...
lock3 hashcodeHex#404b9385
lock2 hashcodeHex#795b7822
lock1 Thread id#12, hashCode#396953361, hashCodeHex#17a90711
lock.SynchronizedDemo$1 object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 d9 19 (00000101 00000000 11011001 00011001) (433651717)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
##########################################
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a 34 ad 17 (01001010 00110100 10101101 00010111) (397227082)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
------------------------------------------
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 10 f0 2a 1b (00010000 11110000 00101010 00011011) (455798800)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
******************************************
疑惑
很多博客上写到偏向锁的前54
位表示线程id
,从jol
打印的线程id
来看,两者并不相等,希望有大佬能留言赐教。
轻量级锁
不同线程交替执行。如果上锁线程未执行完成,又有线程来抢锁,则轻量级锁将升级为重量级锁。
使用CAS
自旋锁方式上锁。
示例代码
public static void lightweightLock() {
Object lock = new Object();
System.out.println("Thread id#" + Thread.currentThread().getId());
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
System.out.println("----------------------------------------------");
new Thread(() -> {
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
}
}, "A").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
}
}, "B").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n##########################################");
}
}, "C").start();
new Thread(() -> {
try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable() + "\n++++++++++++++++++++++++++++++++++++++++++");
}
}, "D").start();
}
头信息
可以看到:
- 未上锁前的锁标志位为
001
(无锁状态) - 线程
A
执行时的锁标志位为000
(轻量级锁) - 两秒后线程
B
在线程A
释放了锁的前提下执行(即:交替执行)时的锁标志位为000
(轻量级锁) - 四秒后线程
C
和线程D
同时启动竞争锁,锁由轻量级升级为重量级,此时线程C
和线程D
的锁标志位都为010
(重量级锁)
Thread id#1
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
----------------------------------------------
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) b0 ef ac 1b (10110000 11101111 10101100 00011011) (464318384)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
##########################################
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) b0 f2 bc 1b (10110000 11110010 10111100 00011011) (465367728)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
##########################################
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a e8 51 03 (00001010 11101000 01010001 00000011) (55699466)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
##########################################
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a e8 51 03 (00001010 11101000 01010001 00000011) (55699466)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
++++++++++++++++++++++++++++++++++++++++++
重量级锁
不同线程竞争同一资源。
效率低下原因:对象内部的一个叫做监视器锁(monitor
)来实现的,但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock
来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到内核态,这个成本非常高,状态之间的转换需要相对较长的时间。
示例代码
public static void heavyweightLock() {
Object lock = new Object();
System.out.println("Thread id#" + Thread.currentThread().getId());
System.out.println("lock hashcode#" + Integer.toHexString(lock.hashCode()));
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
System.out.println("----------------------------------------------");
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
System.out.println("##########################################");
}
}).start();
new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}).start();
}
头信息
Thread id#1
lock hashcode#1540e19d
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 9d e1 40 (00000001 10011101 11100001 01000000) (1088527617)
4 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
----------------------------------------------
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ba 34 f5 17 (10111010 00110100 11110101 00010111) (401945786)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
##########################################
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ba 34 f5 17 (10111010 00110100 11110101 00010111) (401945786)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
其他优化
适应性自旋 Adaptive Spinning
从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
锁粗化 Lock Coarsening
将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。
示例代码:
public void test() {
Object lock = new Object();
synchronized (lock) {
System.out.println("11111111");
}
synchronized (lock) {
System.out.println("22222222");
}
synchronized (lock) {
System.out.println("33333333");
}
}
锁粗化后:
public void test() {
Object lock = new Object();
synchronized (lock) {
System.out.println("11111111");
System.out.println("22222222");
System.out.println("33333333");
}
}
锁消除 Lock Elimination
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。
示例代码:
public class LockEliminationDemo {
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
public static void main(String[] args) {
LockEliminationDemo lockEliminationDemo = new LockEliminationDemo();
long startTs = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
lockEliminationDemo.append("a", "b");
}
System.out.println(System.currentTimeMillis() - startTs);
}
}
输出如下,可以看到JDK1.8
是默认开启锁消除的,关闭锁消除后执行时间上升1.5
倍:
PS D:\src> java LockEliminationDemo
1552
PS D:\src> java -XX:-EliminateLocks LockEliminationDemo
4448
PS D:\src> java -XX:-EliminateLocks LockEliminationDemo
4457
PS D:\src> java -XX:+EliminateLocks LockEliminationDemo
1553
PS D:\src> java -XX:+EliminateLocks LockEliminationDemo
1541
参考
https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf
————        END        ————
Give me a Star, Thanks:)
https://github.com/fendoudebb/LiteNote扫描下方二维码关注公众号和小程序↓↓↓