synchronized锁的四种状态及优化存储

编程入门 行业动态 更新时间:2024-10-08 08:24:18

synchronized锁的<a href=https://www.elefans.com/category/jswz/34/1769239.html style=四种状态及优化存储"/>

synchronized锁的四种状态及优化存储

synchronized 锁升级的过程

在多线程并发编程中synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是随着Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重,Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

那么锁升级过程中的状态存储在哪里呢?

锁对象里面都保存了哪些信息呢?

在Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:

        对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

 

 synchronized用的锁是存在Java对象头里的,Java对象头里面包含两部分信息:
第一部分官方称之为“Mark Word” ,用于存储自身的运行时数据,如:HashCode,GC分代年龄,锁标记、偏向锁线程ID等;第二部分是类型指针,即对象指向它的类元信息,虚拟机通过这个指针来确定这个对象是哪个类的实例(如果java对象是一个数组,那么对象头中还必须有一块用于记录数组长度的数据)
锁是记录在对象头中的“Mark Word”

“Mark Word”又是如何存储锁的信息的呢?

偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,所以为了让线程获得锁的代价更低而引入了偏向锁。

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的线程ID。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

偏向锁的获取

1、首先获取锁对象头中的 Mark Word判断当前对象是否处于可偏向状态(即当前没有对象获得偏向锁)。

2、原子操作成功当前为偏向锁,失败别的线程以获取锁

  •         如果是可偏向状态,则通过CAS原子操作,把当前线程的ID写入到 MarkWord,如果CAS成功,表示获得偏向锁成功,会将偏向锁标记设置为1,且将当前线程的ID写入Mark Word;
  •         如果CAS失败则说明当前有其他线程获得了偏向锁,同时也说明当前环境存在锁竞争,这时候就需要将已获得偏向锁的线程中的偏向锁撤销掉,
  •         并升级为轻量级锁(偏向锁的撤销,需要等待全局安全点,即在这个时间点上没有正在执行的字节码)。

3)   当前线程是已偏向状态,检查Mark Word中的ThreadID是否和自己相等:

        如果相等则不需要再次获得锁,可以直接执行同步代码块,

        如果不相等,说明当前偏向的是其他线程,需要撤销偏向锁并升级到轻量级锁

 

偏向锁 在 java1.6 之后是默认启用的;
但是在应用程序启动几秒钟后才激活,可以使用-XX:BiasedLockingStartupDelay=0参数关闭延迟,
 如果确定应用程序所有锁通常情况下处于竞争状态,可以通过-XX:-UseBiasedLocking参数关闭偏向锁(从JDK15开始,默认禁用了偏向锁因为维护这种锁同步优化的成本太高了)

 

为什么要使用偏向锁?

        偏向锁是 HotSpot 虚拟机使用的一项优化技术,能够减少无竞争锁定时的开销。偏向锁的目的是假定 monitor 一直由某个特定线程持有,直到另一个线程尝试获取它,这样就可以避免获取 monitor 时执行 cas 的原子操作。monitor 首次锁定时偏向该线程,这样就可以避免同一对象的后续同步操作步骤需要原子指令。从历史上看,偏向锁使得 JVM 的性能得到了显著改善。

为什么又要废弃偏向锁?

        虽然过去看到的性能提升,在现在看来已经不那么明显了。受益于偏向锁的应用程序,往往是使用了早期 Java 集合 API的程序(JDK 1.1),这些 API(Hashtable 和 Vector) 每次访问时都进行同步。JDK 1.2 引入了针对单线程场景的非同步集合(HashMap 和 ArrayList),JDK 1.5 针对多线程场景推出了性能更高的并发数据结构(ConcurrentHashMap)。这意味着如果代码更新为使用较新的类,由于不必要同步而受益于偏向锁的应用程序,可能会看到很大的性能提高。此外,围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏向锁的情况下变得更好。

偏向锁为同步系统引入了许多复杂的代码,并且对 HotSpot 的其他组件产生了影响。这种复杂性已经成为理解代码的障碍,也阻碍了对同步系统进行重构。因此,从jdk15开始禁用、废弃并最终删除偏向锁。

偏向锁的撤销

        偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中发现:

  1. CAS失败:存在线程竞争
  2. 被偏向的锁对象升级到被加了轻量级锁的状态
  3. 对原持有偏向锁的线程进行撤销时

原获得偏向锁的线程有两种情况:

        1.同步代码块执行完了,那么这个时候会把对象头设置成无锁状态,同时正在争抢锁的线程可以基于 CAS 重新偏向当前线程。
 
        2.同步代码块还没执行完,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。(锁的膨胀过程,防止其他线程拿到锁)等待执行完后在释放锁,同时正在争抢锁的线程可以基于 CAS 重新偏向当前线程。
 

 

偏向锁及撤销流程图

 

 

偏向锁注意事项

        偏向锁在Java SE 1.6和Java SE 1.7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0

如果你确定应用程序里所有的锁通常情况下都处于竞争状态,     
可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking
那么程序默认会进入轻量级锁状态。
如果我们的应用中大多数情况存在线程竞争建议是关闭偏向锁,因为开启反而会因为偏向锁撤销操作而引起更多的资源消耗。

轻量级锁

轻量级锁,一般用于两个线程在交替使用锁的时候,由于没有同时抢锁,属于一种比较和谐的状态,就可以使用轻量级锁。
 

轻量级锁加锁

线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。

线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针:

  • 如果成功,当前线程获得锁
  • 如果失败,表示其他线程竞争到锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头:

  • 如果成功,则表示没有竞争发生。
  • 如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

 

自旋锁

轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。

为什么要采用自旋等待呢?

因为绝大多数情况下线程获得锁和释放锁的过程都是非常短暂的,自旋一定次数之后极有可能碰到获得锁的线程释放锁,所以,轻量级锁适用于那些同步代码块执行很快的场景,这样,线程原地等待很短的时间就能够获得锁了。

注意:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是 10 次,在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

在高负载、高并发的场景下,可以通过设置 JVM 参数来关闭自旋锁,优化性能:
-XX:-UseSpinning//参数关闭自旋锁优化(默认打开) 
-XX:PreBlockSpin//参数修改默认的自旋次数。JDK1.7后,去掉了此参数,由jvm自适应控制

自适应自旋

在 JDK1.7 开始,引入了自适应自旋锁,修改自旋锁次数的JVM参数被取消,虚拟机不再支持由用户配置自旋锁次数,而是由虚拟机自动调整。自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源
 

 

重量级锁

轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待唤醒了。每一个对象中都有一个Monitor监视器,而Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。而且当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。我们可以简单的理解为,在加重量级锁的时候会执行monitorenter指令,解锁时会执行monitorexit指令。
 

 

锁消除
这属于编译器对锁的优化,JIT 编译器在动态编译同步块时,会使用逃逸分析技术,判断同步块的锁对象是否只能被一个对象访问,没有发布到其它线程。
如果确认没有“逃逸”,JIT 编译器就不会生成 Synchronized 对应的锁申请和释放的机器码,就消除了锁的使用。
锁粗化
JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

⦁    其实现在 Synchronized 的性能并不差,偏向锁、轻量级锁并不会从用户态到内核态的切换;只有在竞争十分激烈的时候,才会升级到重量级锁。
⦁    Synchronized 的锁是由 JVM 实现的。
⦁    偏向锁已经被废弃了。

synchronized优化

减少synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争
尽量让synchronized同步代码块当中的代码少一点这样执行的时间也就会少一点
那么在单位时间内所执行的线程也就多一点;等待的线程也就少一点;
另外由于执行比较短,由轻量级锁就有可能搞得定;或者通过自旋锁就可以搞得定;
避免升级到重量级锁;
synchronized(Demo01.class){
  System.out.println("aaa");
}
降低synchronized锁的粒度
将一个锁拆分为多个锁提高并发度
 

更多推荐

synchronized锁的四种状态及优化存储

本文发布于:2024-03-13 00:24:13,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1732768.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:四种   状态   synchronized

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!