synchronized锁基础篇
1、synchronized的意义
1.1 常见线程安全问题分析
前文已知,volatile关键字可以保证并发三大特性中的有序性、可见性。但更为致命的并发操作的原子性,volatile并不能完全覆盖。今天来看synchronized如何保证操作的原子性。
public class SyncDemo {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
//思考: counter=?
System.out.println(counter);
}
public static void increment() {
counter++;
}
public static void decrement() {
counter--;
}
}
//执行结果(每次各不同、因为是竞态条件,执行得到的结果也会不同)
-1204
Process finished with exit code 0
以上述代码中的++操作为例,看++操作的字节码如下。字节码中很明显的看到++操作指令分4步,JVM如何能让这4步操作是原子的?当单线程执行++操作尚可,当多线程同时执行呢?
0 getstatic #11 从类中获取静态字段
3 iconst_1 将int压到操作数栈
4 iadd 整数相加
5 putstatic #11 将结果设置回类中静态字段
8 return 返回
由下图可见,当多线程操作++方法,由于无法保证操作的原子性,必然导致数据的不安全。
1.2 临界区及竞态条件
多个线程同时对共享资源进行读写操作时,就会出现指令交错,造成线程安全问题。从而达到竞态条件。
为避免临界区的竞态条件发生,一般有多种手段保证:
- 阻塞式方案:synchronized、Lock
- 非阻塞式方案:互斥量、cas等
2、synchronized使用
前文书,synchronized是解决竞态条件的手段之一,我们的生产中也随处可见为了保证操作原子性而加上的锁。
//1、对静态方法加锁 -> 锁类对象(方法是类方法,故锁也是锁住类对象)
public synchronized static void increase1() {
counter++;
}
//2、对实例方法加锁 -> 锁类实例对象(实例方法属于类实例 类实例#方法调用)
public synchronized void increase2() {
counter++;
}
//3、代码块 -> 锁类实例对象
private Object lock = new Object();
public void increase3() {
synchronized (lock){
counter++;
}
}
//4、代码块 -> 锁类对象
public void increase4() {
synchronized (Object.class){
counter++;
}
}
//5、代码块 -> 锁任意实例对象
private String lockStr ="";
public void increase5(){
synchronized (lockStr){
counter++;
}
}
对上述线程安全问题的加锁方法有很多种,但是加锁的方式不同,产生的影响自然也不同。当被锁对象在无意识的情况下,产生竞争,将会导致线程竞争的激烈程度无意识的增加。我们将在下文中展现,当多线程竞争趋于激烈时,开销增大的原因。再遇到非常激烈竞争时(始终cas失败的场景,甚至将导致线程park(),这会导致用户态到内核态的频繁切换,开销极大)
3、synchronized锁状态分析
3.1 对象内存布局
JDK1.5之前synchronized锁的操作很重,主要基于管程思想在JVM上的实现(重量级锁)。但随着Doug Lea在1.5针对synchronized的优化,synchronized存在多种锁状态:无锁、偏向锁、轻量级锁、重量级锁。
JVM针对synchronized优化的意义何在?
- 并发的场景很多,但大多场景中只是基于线程安全的需求加锁,实际上并发量不高。存在并发动辄就park()阻塞线程,让系统中的线程处于阻塞这样开销极大。
- 在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。
- 多线程竞争越激烈,则JVM自适应的选择合适的锁。
既然存在多种锁状态,那么锁状态的信息又存放在哪里? 对象头的Mark word中。
简单分析对象内存布局中各组成部分及其含义:
-
对象头
- Mark word (不同锁状态的Mark word所存储的数据含义是不同的)
32位计算机mark word占4字节32位,64位计算机markword占8字节64位。下图是64位计算机下不同锁状态markword的形态。
锁状态 56bit 1bit 4bit 1bit 2bit 是否偏向锁 锁标记位 无锁态 unused:25bit 对象的hashCode:31bit unused 分代年龄 0 01 偏向锁 线程ID:54bit Epoch:2bit unused 分代年龄 1 01 轻量级锁 指向栈中锁记录的指针(ptr_to_lock_record) 00 重量级锁 指向互斥锁(重量级锁)的指针(ptr_to_heavyweight_monitor) 10 GC标记 空 11 -
Klass Pointer
Klass Pointer指向c++源码中存放对象mate Data的指针地址。 Jdk1.8后默认开启了指针地址压缩,压缩后指针地址占用4字节。当在JVM参数中关闭指针压缩(-XX:-UseCompressedOops)后,长度为8字节。
-
数组长度
如果对象是数组类型,则对象头中还有一块数据存放数组的长度。4字节32位,可以表达长度2^32-1长度
- Mark word (不同锁状态的Mark word所存储的数据含义是不同的)
-
实例数据
存放类的属性信息,包括父类属性信息。
-
对象填充
前文提到过,计算机存储单元以8字节为存储行,偏于运算。所以对象头长度必须是8的倍数,不足8位时自动填充。
3.2 无锁、偏向锁、轻量级锁、重量级锁
<!-- 查看Java 对象布局、大小工具 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
从下面示例中演示4种锁状态,看对象头的分布。
public class SyncOopHeader {
public static void main(String[] args) throws InterruptedException {
//无锁
System.out.println("无锁\n"+ClassLayout.parseInstance(new Object()).toPrintable());
TimeUnit.SECONDS.sleep(5);
Object obj = new Object();
//匿名偏向
System.out.println("匿名偏向\n"+ClassLayout.parseInstance(obj).toPrintable());
new Thread(()->{
synchronized (obj){
//偏向锁
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
//偏向锁退出仍是偏向锁(偏向线程不变)
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
// jvm 优化
// t1释放锁后,t1保活,则线程2变成轻量级锁(jvm认为此时是线程交替抢占锁)
// t1释放锁后,t1不保活,则等待线程1结束后5s,线程2抢占锁,则线程2中锁仍是偏向锁(jvm认为此时仍是单线程加解锁)
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Thread1").start();
//等待5秒脱离线程1的影响
TimeUnit.SECONDS.sleep(5);
Thread t2 = new Thread(()->{
synchronized (obj){
//开始争抢 t1保活时,轻量级锁(线程1解锁5s后,线程2开始争抢锁)
//开始争抢 t1不保活,偏向锁(线程1解锁5s后,线程2开始争抢锁)
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread2");
t2.start();
//等待5s,保持线程3和线程4单独竞争锁
TimeUnit.SECONDS.sleep(5);
//当t3、t4同时开抢 有1个线程持有锁,另一个线程始终无法获取到锁,则从轻量级锁膨胀至重量级锁
new Thread(()->{
synchronized (obj){
//1个线程获取 另一个长时间cas失败 则从轻量级膨胀至重量级锁
System.out.println(Thread.currentThread().getName()+"000"+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread3").start();
new Thread(()->{
synchronized (obj){
//1个线程获取 另一个长时间cas失败 则从轻量级膨胀至重量级锁
System.out.println(Thread.currentThread().getName()+"000"+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread4").start();
}
}
无锁
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 f8 (11100101 00000001 00000000 11111000) (-134217243)
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) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread1
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 ab 22 (00000101 00101000 10101011 00100010) (581642245)
4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread1释放锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 ab 22 (00000101 00101000 10101011 00100010) (581642245)
4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread2
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 80 f0 1f 6e (10000000 11110000 00011111 01101110) (1847586944)
4 4 (object header) f0 00 00 00 (11110000 00000000 00000000 00000000) (240)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread2释放锁
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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread3000
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread4000
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread3释放锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Thread4释放锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
无锁
JVM在开始启动时设置了默认4s长的无锁状态,减少启动时的synchronized竞争。轻量级锁和重量级锁在退出时,也会首先回到无锁状态。(TODO未演示出效果)
电脑是使用小端模式,高8位地址显示在高位。
无锁 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 f8 (11100101 00000001 00000000 11111000) (-134217243) 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) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
偏向锁(偏向线程1)
Thread1 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 d8 ca 3d (00000101 11011000 11001010 00111101) (1036703749) 4 4 (object header) 18 02 00 00 (00011000 00000010 00000000 00000000) (536) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
轻量级锁
Thread2 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 80 f0 1f 6e (10000000 11110000 00011111 01101110) (1847586944) 4 4 (object header) f0 00 00 00 (11110000 00000000 00000000 00000000) (240) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total Thread2释放锁 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 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
-
重量级锁
Thread4000 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346) 4 4 (object header) ca 02 00 00 (11001010 00000010 00000000 00000000) (714) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total Thread4释放锁 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 4a c3 22 21 (01001010 11000011 00100010 00100001) (555926346) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
4、synchronized锁的升级与撤销详解
4.1 无锁状态
- 偏向锁状态下调用#hashCode(),偏向锁无法存储hashCode值,转为无锁。
- jvm启动默认4s延迟开启偏向锁,4s前生成的对象,默认为无锁。
- 轻量级锁退出锁时,状态为无锁。
4.2 偏向锁状态
- 初始化的偏向锁状态为匿名偏向
- 当线程cas 锁对象的线程ID值时,该线程持有锁对象
- 多次重入会记入Epoch值,用于批量重偏向和批量重偏向撤销的统计(25s内)
4.3 轻量级锁
- 轻量级锁适用于线程交替执行synchronized代码块
- 当前线程需要进入锁块,在持有偏向锁对象的线程未消亡时,当前线程copy markword,并将mark word CAS修改为线程栈的lock_record地址,cas成功则轻量级锁持有成功。
- 当前线程重入轻量级锁时,线程栈帧会发生多次锁对象的压栈,线程栈的lock record中存放锁对象、mark word信息。
-
轻量级锁退出锁状态后,markword信息从线程栈的lock_record中拷贝出来,再修改回markword。
-
特别注明:在轻量级线程中,并没有很多cas,更没有cas自旋。
4.4 重量级锁
-
管程思想
-
java中对管程思想的实现有2种方式
管程思想中的关键因素(程序内多线程互斥访问共享资源。只有一个线程允许进入程序,其余线程排队等待。)
- 入口等待队列
- 条件等待队列
-
synchronized的管程由JVM实现
- 入口等待队列(首次触发竞争的线程,先进入cxq排队等待)[cxq是栈帧排列 -> 先入后出,导致不公平]
- waitSet(当持有线程触发等待条件,wait等)[需要等待满足启动的条件(notify等),才能继续争抢线程]
-
AbstractQueueSynchonizer(AQS的实现)
-
重量级锁重入
-
膨胀过程与撤销
5、synchronized锁优化
5.1批量重偏向
Jvm针对偏向锁中Epoch的值进行统计,当25s内有20个锁对象完成线程重偏向,则后续线程将直接升级为轻量级锁。
5.2 批量撤销
当Epoch值在25s内完成40个锁对象的重偏向,则关闭偏向锁,后续新生成的锁对象直接为无锁状态。
5.3 自旋优化
5.4 锁的粗化
出现一个方法内,多次没有必要的加锁时,JVM将自动将多余的锁操作粗化,保留一个即可。
5.5 逃逸分析
当加锁代码块执行的区间无法逃逸出线程栈,此时的加锁是没有意义的。JVM将对该部分锁进行优化。