Synchronized锁升级过程

Synchronized锁升级过程

在上一个文章中介绍了Synchronized的原理和工作方式,但是Synchronized属于重量级锁,会对性能造成较大的影响,因此在jdk6中进行了较大优化,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了**在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。**本文介绍一下Synchronized锁优化的过程。

想要明白synchronized锁升级优化的过程,需要先具备CAS的基础知识。

因此在介绍锁升级过程前首先介绍一下CAS:

CAS

cas的全称是:Compare And Swap(比较如果相同则交换),是现在cpu广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

cas将比较和交换转为原子操作,这个原子操作直接由CPU保证。
cas依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,则将新值B保存到内存中。

unsafe类:

Unsafe是CAS的核心类,CAS操作是执行依赖于Unsafe类的方法。
unsafe类使java语言拥有了像C语言的指针一样的操控内存空间的能力。同时也带来了指针问题。过度的使 用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

乐观锁和悲观锁

正好借此时机,区分一下乐观锁和悲观锁
悲观锁从悲观的角度出发,总是假设最坏的情况。每次拿数据都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞。synchronized就是悲观锁的代表。
乐观锁从乐观的角度出发,每次取数据的时候都认为别人不会修改。就算改了也没关系,重试即可。因此不会上锁。在更新的时候会判断一下再此期间有没有别人修改过这个锁。如果没有人修改过则更新,有人修改过则重试。cas的机制就是乐观锁机制,综合性能较好,但需要多核cpu的支持

使用时机:

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

锁升级过程

经过优化后,线程的锁状态是:
无锁–》偏向锁–》轻量级锁–》重量级锁

偏向锁

偏向锁适合的情况是不存在竞争且锁总是由同一个线程多次获得的情况,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁的偏就是偏心的偏,偏向的偏~~~

这个锁会偏向于第一次获得他的线程,会在对象头中存储锁偏向的线程id,以后该线程进入和退出同步块只需要检查是否为偏向锁,锁标志位以及ThreadID即可。偏向锁可以提高带有同步但没有竞争的程序的性能。

它是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

锁的转态存储在对象头的MarkWord中

获得偏向锁的过程:

  1. 判断是否为可偏向状态—MarkWrod中锁标志是否为01,是否偏向锁是否为1
  2. 如果是可偏向状态,则查看线程id是否为当前线程,如果是则直接执行同步代码块,否则进行步骤三
  3. 通过cas操作竞争锁,如果竞争成功,则将MarkWord中线程ID设置为当前线程ID,然后执行同步代码块
  4. 如果获得偏向锁失败说明有竞争,当达到安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后继续执行阻塞在安全点的线程继续执行。

偏向锁的撤销:

  1. 偏向锁不会主动释放(撤销),只有遇到其他线程竞争时才会执行撤销,由于撤销需要知道当前持有该偏向锁的线程栈状态,因此要等到safepoint时执行,此时持有该偏向锁的线程(T)有两种情况;

  2. 一种是:撤销----T线程已经退出同步代码块,或者已经不再存活,则直接撤销偏向锁,变成无锁状态----该状态达到阈值20则执行批量重偏向

  3. 另一种是升级----T线程还在同步代码块中,则将T线程的偏向锁升级为轻量级锁,当前线程执行轻量级锁状态下的锁获取步骤----该状态达到阈值40则执行批量撤销

偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用 - XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

轻量级锁

引入轻量级锁的目的:在多线程交替执行同步代码快的情况下,尽量避免重量级锁引起的性能消耗。

但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁并不是用来替代重量级锁的。

轻量级锁的获得过程如下:

  1. 判断当前对象是否处于无锁状态,或是由偏向锁状态升级为轻量级锁,如果是,则JVM首先将在当前线程中的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord的拷贝。(官方 把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈 帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。
  2. JVM尝试CAS操作将对象的MarkWord更新为指向为LockRecord的指针。如果成功则表示竞争到锁,则将锁标志位设为00
  3. 如果失败则判断当前对象的MarkWord是否指向当前线程的帧栈(此处需要判断是因为锁重入)如果是,则表名当前线程已经持有了当前对象的锁,则执行同步代码块。
  4. 否则说明该锁对象已经被其他线程抢占了,这时,当前线程开始自旋重试。这里自旋重试次数可以是0,也就意味着发生竞争就会膨胀到重量级锁。当然也可以是某个数,自旋一定次数得不到锁后进行膨胀。
    图示如下:
    在这里插入图片描述

轻量级锁的撤销

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级 锁。

也就是说需要CAS将MarkWord恢复回对象头。如果恢复成功,则表示成功解锁。恢复失败,则表示已升级为重量级锁,进入重量级锁的解锁流程。

重量级锁

当对象的锁为重量级锁的时候,MarkWord存放了指向Monitor的指针,这个Monitor实际上就是对象的锁信息。它包含了:持有锁的线程,想要持有锁但被阻塞的队列EntryList以及处于waiting状态的线程。这也就不难理解为什么使用了obj.wait()的时候,会直接升级到重量级锁,因为其他状态的下没有waitSet啊,那我在那里等着被唤醒嘛。
在这里插入图片描述

自旋锁

上篇文章我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从 用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋) , 这项技术就是所谓的自旋锁。

自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在 JDK 6中 就已经改为默认开启了。

自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而 不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果在多线程交替执行同步块的情况下(适合轻量级锁情况),可以避免重量级锁引起的性能消耗。

自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值 是10次,用户可以使用参数-XX : PreBlockSpin来更改。

适应性自旋

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

其他优化技术
锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定

锁粗化

JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这样只需要加一次锁即可。

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

平时使用Synchronized优化:
  1. 尽量减少synchronized的范围
  2. 降低synchronized锁的粒度
  3. 读写分离

参考文章文献:

  1. https://baijiahao.baidu.com/s?
  2. id=1683595351167222334&wfr=spider&for=pc
  3. https://www.jianshu.com/p/5917486df9cc
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值