摘要:前置知识点对象头要了解锁优化策略中的轻量级锁与偏向锁的原理和运作过程,需要先了解虚拟机的对象头部分的内存布局。否则说明这个锁对象已经被其他线程抢占了。
前置知识点:对象头
要了解锁优化策略中的轻量级锁与偏向锁的原理和运作过程,需要先了解Hotspot虚拟机的对象头部分的内存布局。
对象头(摘自《深入理解java虚拟机》)对象头信息是与对象自身定义的数据无关的额外存储成本
如果对象是数组类型,则虚拟机用3个Word(字宽,在32位虚拟机中,一字宽等于四字节,即32bit)存储对象头。如果对象是非数组类型,则用2Word存储对象头。一个额外的字宽用于存储数组长度。
第一个字宽,用来存储对象自身的运行时数据 如:哈希吗(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,简称“Mark Word”。
第二个字宽用于存储指向方法区对象类型数据的指针。
对象头信息会根据对象的状态复用自己的存储空间。例如:在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32bit空间中的25bit用于存储对象哈希吗(HashCode),4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0。
整个对象头如下图:第三行即为第三部分,非数组类型的没有第三个字宽。
Mark Word的默认存储结构如下图:
在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
锁状态Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,
所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率,下文会详细分析。
偏向锁Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
配置默认-XX:+UseBiasedLocking=true
-XX:-UseBiasedLocking=false关闭偏向锁
应用程序启动几秒钟之后才激活
-XX:BiasedLockingStartupDelay = 0关闭延迟
根据上图,我们可以看到,首先判断锁对象是不是可偏向对象(若锁对象已经被轻量级锁定或者重量级锁定了,因为锁不会降级,所以它是不可偏向,同样的,关闭了偏向锁的设置-UseBiasedLocking=false,也会造成锁对象不可偏向)
接下来,我们假定锁对象处于可偏向状态,并且ThreadID为0即biasable & unbiased状态(这里不讨论epoch和age)
当一个线程试图锁住一个处于biasable & unbiased状态的对象时,通过一个CAS将自己的ThreadID放置到Mark Word中相应的位置,如果CAS操作成功进入第(3)步否则进入(4)步
当进入到这一步时代表当前没有锁竞争,锁对象继续保持biasable可偏向状态,但是这时ThreadID字段被设置成了偏向锁所有者的ID,然后进入到第(6)步
当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程在持有偏向锁所有权。这个时候,就要判断持有偏向锁的线程是否还活着,因为一个线程执行完同步代码块后,不会主动释放偏向锁。如果持有偏向锁的线程还活着,将偏向锁消除,膨胀为轻量级锁,否则,将偏向锁消除,让争锁的线程持有偏向锁。
具体过程是:当到达全局安全点(safepoint,在这个时间点上没有字节码正在执行)时拥有偏向锁的线程被挂起,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,锁对象有可能升级为轻量级锁状态(锁标志位置为00),阻塞在安全点的原持有线程被释放,进入到轻量级锁的执行路径中,继续往下执行同步代码。
当一个线程试图锁住一个处于biasable & biased并且ThreadID不等于自己的ID时,这时由于存在锁竞争必须进入到第(4)步来撤销偏向锁。
运行同步代码块
轻量级锁轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
加锁过程在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁记录目前的Mark Word的拷贝(称为Displaced Mark Word)以及记录锁对象的指针owner。
当一个线程来获取这个锁,虚拟机将使用CAS操作尝试将锁对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下:
如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果指向,说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
否则说明这个锁对象已经被其他线程抢占了。那么它就会自旋等待锁,一定次数后仍未获得锁对象,则修改mark word,将其修改为重量级锁的指针,表示该锁对象进入了重量锁状态。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。
由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间的,另一边,持有轻量级锁的线程,之前在获取锁的时候它拷贝了锁对象头的mark word,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对mark word做了修改,两者比对发现不一致导致释放锁的CAS失败,于是也切换到重量锁,释放轻量锁,并唤醒阻塞的线程。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/71559.html
摘要:这两种策略的区别就在于,公平策略会让等待时间长的线程优先执行,非公平策略则是等待时间长的线程不一定会执行,存在一个抢占资源的问题。 之前有一篇文章我们简单的谈到了Java中同步的问题,但是可能在平常的开发中,有些理论甚至是某些方式是用不到的,但是从程序的角度看,这些理论思想我们可以运用到我们的开发中,比如是不是应该一谈到同步问题,就应该想到用synchronized?,什么时候应该用R...
摘要:关于,最后有两点规律需要注意当的等待队列队首结点是共享结点,说明当前写锁被占用,当写锁释放时,会以传播的方式唤醒头结点之后紧邻的各个共享结点。当的等待队列队首结点是独占结点,说明当前读锁被使用,当读锁释放归零后,会唤醒队首的独占结点。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首发于一世流云的专栏:...
摘要:自选锁锁膨胀后,虚拟机为了避免线程真实地在操作系统层面挂起,虚拟机还会在做最后的努力自选锁。 showImg(https://segmentfault.com/img/remote/1460000016159660?w=500&h=333); 作为一款公用平台,JDK 本身也为并发程序的性能绞尽脑汁,在 JDK 内部也想尽一切办法提供并发时的系统吞吐量。这里,我将向大家简单介绍几种 J...
摘要:以下为大家整理了阿里巴巴史上最全的面试题,涉及大量面试知识点和相关试题。的内存结构,和比例。多线程多线程的几种实现方式,什么是线程安全。点击这里有一套答案版的多线程试题。线上系统突然变得异常缓慢,你如何查找问题。 以下为大家整理了阿里巴巴史上最全的 Java 面试题,涉及大量 Java 面试知识点和相关试题。 JAVA基础 JAVA中的几种基本数据类型是什么,各自占用多少字节。 S...
阅读 2643·2021-11-25 09:43
阅读 2432·2021-09-22 15:29
阅读 963·2021-09-22 15:17
阅读 3586·2021-09-03 10:36
阅读 2201·2019-08-30 13:54
阅读 1721·2019-08-30 11:23
阅读 1145·2019-08-29 16:58
阅读 1272·2019-08-29 16:14