Synchronized重量级锁优化之路

在日常过程中经常会遇到并发问题,一个代码块被多个线程持有和修改导致最终结果出现错误,我时候我们一般会想到锁住代码块来解决并发场景问题。

锁分很多种定义,如轻量级锁、自旋锁、偏向锁、重量级锁,在算法实现上又可分为乐观锁、悲观锁,今天我们主要来说说Synchronized,我们都知道synchronized是一个悲观锁,在SDK1.5他是重量级锁,但1.6以后对synchronized做了优化,其初始状态为无锁状态。

1、Synchronized优化后的加锁顺序

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。

JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

2、偏向锁

本质: 在无竞争情况下把整个同步都消除掉,甚至连CAS操作都不做了,只需判断 Mark Word 中的一些值是否正确就行 。进一步提升程序性能。

与轻量级锁的区别:轻量级锁是在无竞争的情况下使用 CAS操作 来代替 互斥同步((阻塞) 的使用,从而实现同步;而偏向锁是 在无竞争的情况下完全取消同步 。

与轻量级锁的相同点:它们都是 乐观锁 ,都认为同步期间不会有其他线程竞争锁。

原理:当线程请求到锁对象后,将锁对象的状态标志位改为 01 , 即进入 “偏向锁状态” 。然后使用 CAS操作 将 线程ID 记录在锁对象的 Mark Word 中。该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 MarkWord 的锁标记位为偏向锁 以及 当前线程 Id 等于 Mark Word 的 ThreadId 即可 直接接进入同步块,连CAS操作都不需要。但是,一旦有第2个线程需要竞争锁,那么偏向模式立即结束, 进入 “轻量级锁” 的状态。

优点:偏向锁可以 提高有同步但没有竞争的程序性能 。但是如果锁对象时常被多个线程竞争,那偏向锁就是 多余 的。偏向锁JDK1.6之后默认开启。参数开启方式:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 在 JDK1.8 中,其实默认是轻量级锁 ,但如果设定了 -XX:BiasedLockingStartupDelay = 0,那在对一个 Object 做 syncronized 的时候,会立即上一把偏向锁。tips:偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会 偏向于第一个获得它的线程 ,如果在当前线程的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步

3、轻量级锁

背景:『轻量级锁』是相对于『重量级锁』而言的,即使用操作系统来实现的传统锁

本质: 在无竞争的情况下使用CAS操作去取代同步使用的 。

轻量级锁与重量级锁的区别:重量级锁是一种 悲观锁 ,它认为总是有多个线程要竞争锁,所以它每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用 互斥同步(阻塞) 来保证线程的安全;而轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,所以它不使用互斥同步,而是 使用CAS操作来获得锁 ,这样能 减少传统的重量级锁使用操作系统 『』 带来的性能开销。

实现原理:对象头称为『 Mark Word 』,对象处于不同的状态下, Mark Word 中存储的信息也所有不同。Mark Word中有个 标志位 用来表示当前对象所处的状态。当线程请求锁时,若该锁对象的Mark Word中标志位为 01 ( 无锁状态 ),则在该线程的栈帧中创建一块名为 『锁记录Lock Record』 的空间,然后将 锁对象的Mark Word拷贝至该空间 ; 最后通过 CAS操作 将锁对象的 Mark Word 更新为指向Lock Record的指针若CAS更新指针 成功 ,则轻量级锁的上锁过程成功;若CAS更新指针 失败 ,再判断 当前线程是否已经持有了该轻量级锁 ;若已经持有, 直接进入同步块 ;若尚未持有,则表示该锁已经被其他线程占用, 此时轻量级锁就要膨胀成 “重量级锁”。

轻量级锁比重量级锁性能更高的前提 :在轻量级锁被占用的整个同步周期内,不存在其他线程的竞争 。若在该过程中一旦有其他线程竞争,那么就会 膨胀成重量级锁 ,从而除了使用以外,还额外发生了 CAS操作 , 因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。如果执行同步块的时间 比较短 ,那么多个线程之间执行使用轻量级锁 交替执行 。如果执行同步块的时间 比较长 ,那么多个线程之间刚开始使用轻量级锁,后面会 膨胀为重量级锁 。(因为执行同步块的时间长,线程 CAS 自旋获得轻量级锁失败后就会锁膨胀)

4、自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

5、重量级锁

重量级锁是一种 悲观锁,它认为总是有多个线程要竞争锁,所以它每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用 互斥同步(阻塞)来保证线程的安全;

6、锁升级顺序

syncronized一共有 4 种锁状态,级别从低到高依次是: 无锁->偏向锁->轻量级锁->重量级锁,这几个状态会随着竞争情况逐渐升级。

锁可以升级但不能降级 ,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略, 目的是为了提高获得锁和释放锁的效率。

偏向锁的判断
在 JDK1.8 中,其实默认是轻量级锁 ,但如果设定了 -XX:BiasedLockingStartupDelay = 0 ,那在无竞争的时候对一个 Object 做 syncronized 的时候,会立即上一把偏向锁。 当处于偏向锁状态时, MarkWork 会记录当前线程 ID 。

升级到轻量级锁的判断
一旦有 第2个线程 参与到偏向锁竞争时,会先判断 MarkWork 中保存的 线程 ID 是否与这个线程 ID 相等 , 如果不相等,会立即撤销偏向锁,升级为轻量级锁 。每个线程在自己的 线程栈中 生成一个 LockRecord ( LR ) ,然后每个线程通过 CAS (自旋) 的操作将锁对象头中的 MarkWork 设置为指向自己的 LR 的指针, 哪个线程设置成功,就意味着获得锁 。

升级到重量级锁的判断
如果锁竞争加剧( 如线程自旋次数或者自旋的线程数超过某阈值, JDK1.6 之后,由 JVM 自己控制该规则 ),就会升级为重量级锁 。此时就会向操作系统申请资源,线程挂起,进入到操作系统内核态 的 等待队列 中,等待操作系统调度,然后映射回 用户态在重量级锁中,由于 需要做内核态到用户态的转换 ,而这个过程中需要消耗较多时间,有可能比用户执行代码的时间还要长。也就是"重"的原因之一。重量级锁通过是对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的实现实现.

7、锁的优劣

各种锁并不是相互代替的,而是在不同场景下的不同选择,绝对不是说重量级锁就是不合适的。每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。

  1. 如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;
  2. 如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;
  3. 如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;

在第3种情况下进入同步代码块就 要做偏向锁建立、偏向锁撤销、轻量级锁建立、升级到重量级锁,最终还是得靠重量级锁来解决问题,那这样的代价就比直接用重量级锁要大不少了。所以使用哪种技术,一定要看其所处的环境及场景,在绝大多数的情况下,偏向锁是有效的,这是基于HotSpot作者发现的“大多数锁只会由同一线程并发申请”的经验规律

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值