Synchronized与锁升级

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

一 高并发时锁的使用

同步调用应该去考量锁的性能的损耗,能用无锁数据结构,就不要用锁,能锁区块,就不要锁整个方法体,能用对象锁,就不要用类锁。

锁升级的过程

synchronized锁(重量级锁):由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略。

java5只有重量锁可以用.

java6引入了偏向锁和轻量级锁。

锁的是用有一个逐步升级的过程,不能直接使用sync重量级锁。

二 为什么每一个对象都可以成为一个锁

每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。

Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。

三 synchronized锁种类及升级步骤

1 升级流程

synchronized用的锁是存在Java对象头里的Mark Word中。

锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位。

下图:(重要

2 无锁

不使用锁,直接代码执行。

从图中value的进行解析:(结合上面的64位图使用)(只有用到hashcode第一行和第二行才有数据)【从第二行倒数往前看为顺序】

从第二行开始,第一段数据00100011到第一行的01011110这一段是hashcode的信息,而前面的暗色的001是偏向锁位和锁的标志位(代表无锁),第二行的后面的后三段00000000是上图的25位,因为只有24,所以要在第一段数据补0。

3 偏向锁

作用:

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁

在多线程情况下,锁不仅不会竞争,还存在锁由同一线程多次获得的情况,偏向锁就此产生。

也就意味着:自始至终使用锁的线程只有一个,偏向锁没有额外的开销,性能极高。

但是如果发生了竞争,当1线程和2线程抢夺资源的时候,锁就不是偏向同一个线程了,这时候就不是偏向锁了,需要进行锁升级。升级为轻量级锁。才能保证公平。(偏向锁不会主动释放)

**目的:**为了解决只有在一个线程执行同步时提高性能。

**偏向锁的实现方式:**通过CAS方式修改markword中的线程ID

执行过程(以account对象举例)

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例。

假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。

执行完同步代码块后,线程并不会主动释放偏向锁。这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入。上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

偏向锁JVM命令

java -XX:+PrintFlagsInitial |grep BiasedLock*(查询java信息中偏向锁的信息)

实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟。

所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。

开启偏向锁

XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:

关闭之后程序默认会直接进入轻量级锁状态

-XX:-UseBiasedLocking

关闭延迟(演示偏向锁开启)

-XX:BiasedLockingStartupDelay=0

**要关闭延迟的原因:**在jdk1.6之后,程序启动几秒之后才会激活,在JVM中关闭。这时候才会实时的显示偏向锁的偏移为101。

偏向锁的撤销

竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。

偏向锁的撤销偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

① 第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

② 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。

红线部分为偏向锁获取和撤销过程:

4 轻量级锁

本质:自旋锁

标志位:0 0;

升级到重量级锁:先自旋再阻塞。

长期的自旋,会空耗cpu的资源,自旋的时候是有一定的次数,如果次数内不成功,就升级为重锁。

在jdk6之前:默认启用,默认的自旋次数10次。或者自选线程数超过cpu核数的一半(了解即可)

jdk6之后:根据同一个自旋锁上一次自旋的时间和拥有锁线程的状态来决定。(自适应)

轻量锁与偏向锁的区别和不同(重)

1 争夺轻量级锁失败时,自旋尝试抢占锁。

2 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。

5 重量级锁

什么时候需要使用?

大量线程参与竞争。使用sync。

总结

synchronized的升级过程:先自旋,不行再阻塞。

四 锁粗化和锁消除

JIT:just in time Compiler(即时编译器)

1
2
3
vbnet复制代码12345678910111213141516171819

//锁消除public class SafeDoubleCheckTwo { static Object object = new Object(); public void test(){ Object object = new Object(); //锁消除 synchronized (object){ } } public static void main(String[] args) { } }Copy

锁消除理解:方法中锁住的对象没有被共用扩散,不管被当前线程或者下一个线程访问,都没什么区别,每个人都可以拿一份锁,都可以进入新的对象空间,没意义。

锁失去了作用,即锁消除。

锁粗化理解:假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块, 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%