在java并发中synchronized一直是一个重要的角色,有人称它为重量级锁,但在jdk1.6之后synchronized得到了优化,引入了偏向锁和轻量级锁,避免线程上下文切换带来的耗时,所以看起来就没有那么重了。
对象的组成
因synchronized锁信息都是保存在对象头部中,故而先从对象头入手。
对象的组成:
- 头部信息(头部信息分为两块Mark Word(如图)与类型指针(java虚拟机默认开启类型指针的压缩(4) )
- 实例数据(各种类型的变量)
- 对其填充(起到占位符的作用,主要是因为HotSpot虚拟机的内存管理要求对象的大小必须是8字节的整数倍,而对象头正好是8个字节的整数倍,但是实例数据不一定,所以需要对齐填充补全。)
由图上可知,Mard Word中保存着锁的信息、分代年龄、hashCode等;Biased是偏向锁,标志位为101,54位thread用力存储线程的id;Lightweight Locked是轻量级锁,标志位为00,ptr_to_lock_record用来存储指向锁记录的指针;Heavyweight Locked是重量级锁,标志位为10,ptr_to_heavyweight_monitor用来存储指向锁记录的指针。
CAS
在优化synchronized过程中大量使用到CAS操作。CAS全称(Compare And Set),CAS是一种乐观锁操作并且包含三个操作数:内存位置(V)、原值(A)、新值(B)。当条件有且只能满足V与A相等时,才会将B赋值给A。
AtomicInteger当中常用的自增方法 incrementAndGet:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) return next;
}
}
private volatile int value;
public final int get() {
return value;
}
CAS的自旋。循环体当中做了三件事:
- 获取当前值。
- 当前值+1,计算出目标值。
- 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤。
轻量级锁
一个对象虽然有多个线程加锁,但是加锁的时间是错开的(也就是没有锁竞争),那么会使用轻量级锁来优化,轻量级锁是透明的,语法仍然是synchronized
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
结果如图mark word信息000000000370f0f0转为二进制11011100001111000011110000为轻量级锁:
轻量级锁执行步骤
- 当前线程创建锁记录(包含锁记录地址和锁的标志位)
- 尝试cas原子操作,把锁记录信息存入对象头的mark word中,并将mark word中的信息存入当前线程的锁记录中。
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁
- 如果 cas 失败,有两种情况;1.如果是其它线程已经持有了该 对象的轻量级锁,这时表明有竞争,进入锁膨胀过程升级为重量级锁;2.如果是自己执行了 synchronized 锁重入,那么再添加一条 锁记录作为重入的计数。
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一,使用cas将锁记录中的信息恢复到mark word中。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
锁膨胀过程
- 当前线程进行轻量级加锁时,别的线程已经对该对象加了轻量级锁。
- 当前线程加锁失败,进入膨胀流程,即当前对象申请Moniter锁,当前加锁对象指针指向重量级锁地址。
- 当别的线程退出同步块时,使用cas操作将获取的mark word值恢复到mark word中失败,这时会进入重量解锁流程,按照Moniter地址找到Moniter对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
自旋锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋成功情况:
线程 1 (core 1 上) | 对象 Mark Word | 线程 2 (core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁) | - |
成功(加锁) | 10(重量锁) | - |
执行同步块 | 10(重量锁) | - |
执行同步块 | 10(重量锁) | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁) | 自旋重试 |
执行完毕 | 10(重量锁) | 自旋重试 |
成功(解锁) | 01(无锁) | 自旋重试 |
- | 10(重量锁) | 成功(加锁) |
- | 10(重量锁) | 执行同步块 |
自旋失败情况:
线程 1 (core 1 上) | 对象 Mark Word | 线程 2 (core 2 上) |
---|---|---|
- | 10(重量锁) | - |
访问同步块,获取 monitor | 10(重量锁) | - |
成功(加锁) | 10(重量锁) | - |
执行同步块 | 10(重量锁) | - |
执行同步块 | 10(重量锁) | 访问同步块,获取 monitor |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 自旋重试 |
执行同步块 | 10(重量锁) | 阻塞 |
执行同步块 | 10(重量锁) | 阻塞 |
偏向锁
轻量级锁在没有竞争时(就当前自己这把锁),为了避免每次获取锁都需要多次执行cas原子指令操作,jdk1.6之后引入了偏向锁。
偏向锁执行步骤:
- 首先判断mark word是否是偏向状态,是否偏向为1,锁标志位为01。
- 进入偏向状态后,判断当前线程id与mark word中线程id是f否相等,相等则为偏向锁
- 若线程id不等,则尝试采用cas操作改变 mark word中线程id,成功则为偏向锁
- 若 失败,说明当前线程存在锁竞争关系,则转换为轻量级锁。
public static void main(String[] args) {
Object o=new Object();
//o.hashCode();
new Thread(()->{
System.out.println("synchronized前..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o){
System.out.println("synchronized中..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
System.out.println("synchronized后..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}).start();
}
偏向锁默认是延迟的,程序启动不会立即生效,启用参数-XX:BiasedLockingStartupDelay=0
禁用延迟,运行如上代码结果如图:
- synchronized前,使用JOL查看对象的内存布局,对象头信息(0000000000000005)转成二进制是101可知,对象默认开启的是偏向锁(是否偏向1,标志位为01)。
- synchronized中,头部信息发生了改变000000001b0ad805,转成二进制为11011000010101101100000000101,依然是偏向锁,但是多了当前线程的id(1101100001010110110)(id为54位,0补全)。
- synchronized后,释放锁,但是线程id依然存放在mark word中。
撤销偏向锁(hashCode)
如上偏向锁程序打开注释调用Object的hashCode()方法,代码就不贴出了,直接查看执行结果:
- synchronized前,将mark word中0x00000053e25b7601信息转成二进制为101001111100010010110110111011000000001,根据后三位可知是无锁状态。
- synchronized中,mark word信息000000001c91f460转成二进制11100100100011111010001100000,根据后三位可知是轻量级锁。
- synchronized后,释放锁,轻量级锁转变为无锁状态。
调用Object的hashCode()方法会撤销偏向锁,因hashCode值wark word中为31位,线程id是54位,存储空间不够,所以会转为轻量级锁,将hashCode值存入当前线程的锁记录中。
撤销偏向锁(其它线程使用对象)
private static Thread a,b;
public static void main(String[] args) {
Object o=new Object();
a = new Thread(() -> {
synchronized (o) {
System.out.println("a线程:" + ClassLayout.parseInstance(o).toPrintable());
}
LockSupport.unpark(b);//唤醒线程
}, "a");
a.start();
b = new Thread(() -> {
LockSupport.park();//阻塞线程
System.out.println("synchronized前..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println("synchronized中..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
System.out.println("synchronized后..");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}, "b");
b.start();
}
执行结果如图,从偏向锁转为了轻量级锁。
批量重偏向:
启动程序设置jvm参数-XX:+PrintFlagsFinal
- intx BiasedLockingBulkRebiasThreshold = 20 默认偏向锁批量重偏向阈值
- intx BiasedLockingBulkRevokeThreshold = 40 默认偏向锁批量撤销阈值
public static void main(String[] args) throws InterruptedException {
Thread.sleep(3000);
List<Object> list = new ArrayList<>();
Thread a = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Object o = new Object();
list.add(o);
synchronized (o) {
System.out.println("a"+i + "\t" + ClassLayout.parseInstance(o).toPrintable());
}
}
synchronized (list){
list.notify();
}
}, "a");
a.start();
Thread b = new Thread(() -> {
synchronized (list){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 30; i++) {
Object o = list.get(i);
synchronized (o) {
if( i==19 || i==20) {
System.out.println("b" + i + "\t" + ClassLayout.parseInstance(o).toPrintable());
}
}
}
}, "b");
b.start();
//b.join();
//System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
}
结果如图,当阈值满足于20,当前线程b进入偏向状态:
批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
如上代码注释拿掉运行,epoch不等于0,表示新创建的对象不可偏向。