java 资源锁_Java性能之synchronized锁的优化

synchronized / Lock

1.JDK 1.5之前,Java通过synchronized关键字来实现锁功能

synchronized是JVM实现的内置锁,锁的获取和释放都是由JVM隐式实现的

2.JDK 1.5,并发包中新增了Lock接口来实现锁功能

提供了与synchronized类似的同步功能,但需要显式获取和释放锁

3. Lock同步锁是基于Java实现的,而synchronized是基于底层操作系统的Mutex Lock实现的

每次获取和释放锁都会带来用户态和内核态的切换,从而增加系统的性能开销

在锁竞争激烈的情况下,synchronized同步锁的性能很糟糕

在JDK 1.5,在单线程重复申请锁的情况下,synchronized锁性能要比Lock的性能差很多

4.JDK 1.6,Java对synchronized同步锁做了充分的优化,甚至在某些场景下,它的性能已经超越了Lock同步锁

实现原理

public class SyncTest {

public synchronized void method1() {

}

public void method2() {

Object o = new Object();

synchronized (o) {

}

}

}

$ javac -encoding UTF-8 SyncTest.java

$ javap -v SyncTest

修饰方法

public synchronized void method1();

descriptor: ()V

flags: ACC_PUBLIC, ACC_SYNCHRONIZED

Code:

stack=0, locals=1, args_size=1

0: return

JVM使用ACC_SYNCHRONIZED访问标识来区分一个方法是否为同步方法

在方法调用时,会检查方法是否被设置了ACC_SYNCHRONIZED访问标识

如果是,执行线程会将先尝试持有Monitor对象,再执行方法,方法执行完成后,最后释放Monitor对象

修饰代码块

public void method2();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=4, args_size=1

0: new #2 // class java/lang/Object

3: dup

4: invokespecial #1 // Method java/lang/Object."":()V

7: astore_1

8: aload_1

9: dup

10: astore_2

11: monitorenter

12: aload_2

13: monitorexit

14: goto 22

17: astore_3

18: aload_2

19: monitorexit

20: aload_3

21: athrow

22: return

synchronized修饰同步代码块时,由monitorenter和monitorexit指令来实现同步

进入monitorenter指令后,线程将持有该Monitor对象,进入monitorexit指令,线程将释放该Monitor对象

管程模型

1.JVM中的同步是基于进入和退出管程(Monitor)对象实现的

2.每个Java对象实例都会有一个Monitor,Monitor可以和Java对象实例一起被创建和销毁

3.Monitor是由ObjectMonitor实现的,对应ObjectMonitor.hpp

4.当多个线程同时访问一段同步代码时,会先被放在EntryList中

5.当线程获取到Java对象的Monitor时(Monitor是依靠底层操作系统的Mutex Lock来实现互斥的)

线程申请Mutex成功,则持有该Mutex,其它线程将无法获取到该Mutex

6.进入WaitSet

竞争锁失败的线程会进入WaitSet

竞争锁成功的线程如果调用wait方法,就会释放当前持有的Mutex,并且该线程会进入WaitSet

进入WaitSet的进程会等待下一次唤醒,然后进入EntryList重新排队

7.如果当前线程顺利执行完方法,也会释放Mutex

8.Monitor依赖于底层操作系统的实现,存在用户态和内核态之间的切换,所以增加了性能开销

d5dfe95c8cff405d8f8eff626955b0a8.png

ObjectMonitor() {

_header = NULL;

_count = 0; // 记录个数

_waiters = 0,

_recursions = 0;

_object = NULL;

_owner = NULL; // 持有该Monitor的线程

_WaitSet = NULL; // 处于wait状态的线程,会被加入 _WaitSet

_WaitSetLock = 0 ;

_Responsible = NULL ;

_succ = NULL ;

_cxq = NULL ;

FreeNext = NULL ;

_EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList

_SpinFreq = 0 ;

_SpinClock = 0 ;

OwnerIsThread = 0 ;

_previous_owner_tid = 0;

}

锁升级优化

为了提升性能,在JDK 1.6引入偏向锁、轻量级锁、重量级锁,用来减少锁竞争带来的上下文切换

借助JDK 1.6新增的Java对象头,实现了锁升级功能

Java对象头

在JDK 1.6的JVM中,对象实例在堆内存中被分为三部分:对象头、实例数据、对齐填充

对象头的组成部分:Mark Word、指向类的指针、数组长度(可选,数组类型时才有)

Mark Word记录了对象和锁有关的信息,在64位的JVM中,Mark Word为64 bit

锁升级功能主要依赖于Mark Word中锁标志位和是否偏向锁标志位

synchronized同步锁的升级优化路径:偏向锁 -> 轻量级锁 -> 重量级锁

348c0d3bdc06b470a5b48a23d021446e.png

偏向锁

偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源

偏向锁的作用

当一个线程再次访问同一个同步代码时,该线程只需对该对象头的Mark Word中去判断是否有偏向锁指向它

无需再进入Monitor去竞争对象(避免用户态和内核态的切换)

当对象被当做同步锁,并有一个线程抢到锁时

锁标志位还是01,是否偏向锁标志位设置为1,并且记录抢到锁的线程ID,进入偏向锁状态

偏向锁不会主动释放锁

当线程1再次获取锁时,会比较当前线程的ID与锁对象头部的线程ID是否一致,如果一致,无需CAS来抢占锁

如果不一致,需要查看锁对象头部记录的线程是否存活

如果没有存活,那么锁对象被重置为无锁状态(也是一种撤销),然后重新偏向线程2

如果存活,查找线程1的栈帧信息

如果线程1还是需要继续持有该锁对象,那么暂停线程1(STW),撤销偏向锁,升级为轻量级锁

如果线程1不再使用该锁对象,那么将该锁对象设为无锁状态(也是一种撤销),然后重新偏向线程2

一旦出现其他线程竞争锁资源时,偏向锁就会被撤销

偏向锁的撤销可能需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法

如果还没有执行完,说明此刻有多个线程竞争,升级为轻量级锁;如果已经执行完毕,唤醒其他线程继续CAS抢占

在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大了性能开销

默认配置

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000

默认开启偏向锁,并且延迟生效,因为JVM刚启动时竞争非常激烈

关闭偏向锁

-XX:-UseBiasedLocking

直接设置为重量级锁

-XX:+UseHeavyMonitors

红线流程部分:偏向锁的获取和撤销

dc9dab7d4f6fe58527af2fc66782bc9e.png

轻量级锁

当有另外一个线程竞争锁时,由于该锁处于偏向锁状态

发现对象头Mark Word中的线程ID不是自己的线程ID,该线程就会执行CAS操作获取锁

如果获取成功,直接替换Mark Word中的线程ID为自己的线程ID,该锁会保持偏向锁状态

如果获取失败,说明当前锁有一定的竞争,将偏向锁升级为轻量级锁

线程获取轻量级锁时会有两步

先把锁对象的Mark Word复制一份到线程的栈帧中(DisplacedMarkWord),主要为了保留现场!!

然后使用CAS,把对象头中的内容替换为线程栈帧中DisplacedMarkWord的地址

场景

在线程1复制对象头Mark Word的同时(CAS之前),线程2也准备获取锁,也复制了对象头Mark Word

在线程2进行CAS时,发现线程1已经把对象头换了,线程2的CAS失败,线程2会尝试使用自旋锁来等待线程1释放锁

轻量级锁的适用场景:线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争

红线流程部分:升级轻量级锁

7e03f45ed3a390dab4e46b1043682022.png

自旋锁 / 重量级锁

轻量级锁CAS抢占失败,线程将会被挂起进入阻塞状态

如果正在持有锁的线程在很短的时间内释放锁资源,那么进入阻塞状态的线程被唤醒后又要重新抢占锁资源

JVM提供了自旋锁,可以通过自旋的方式不断尝试获取锁,从而避免线程被挂起阻塞

从JDK 1.7开始,自旋锁默认启用,自旋次数不建议设置过大(意味着长时间占用CPU)

-XX:+UseSpinning -XX:PreBlockSpin=10

自旋锁重试之后如果依然抢锁失败,同步锁会升级至重量级锁,锁标志位为10

在这个状态下,未抢到锁的线程都会进入Monitor,之后会被阻塞在WaitSet中

在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能

一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于CAS重试状态,占用CPU资源

在高并发的场景下,可以通过关闭自旋锁来优化系统性能

-XX:-UseSpinning

关闭自旋锁优化

-XX:PreBlockSpin

默认的自旋次数,在JDK 1.7后,由JVM控制

eda43a05f340d59028b338a240448aa1.png

小结

1.JVM在JDK 1.6中引入了分级锁机制来优化synchronized

2.当一个线程获取锁时,首先对象锁成为一个偏向锁

这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换

3.如果有多个线程竞争锁资源,锁将会升级为轻量级锁

这适用于在短时间内持有锁,且分锁交替切换的场景

轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换

4.如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁

5.优化synchronized同步锁的关键:减少锁竞争

应该尽量使synchronized同步锁处于轻量级锁或偏向锁,这样才能提高synchronized同步锁的性能

常用手段

减少锁粒度:降低锁竞争

减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁

6.在锁竞争激烈时,可以考虑禁用偏向锁和禁用自旋锁

我是小架,我们

下篇文章见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值