批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。 这时候所有的对象a都是偏向线程1的,打印出偏向锁没有问题。 前面19个a都偏向锁升级为轻量锁,达到重偏向阈值,第20个a直接重偏向到t2 打印list中第11个对象的对象头: A object internals: OFFSET SIZE TYPE t2竞争了前30个对象a,第0~18个对象a未达到重偏向阈值,是轻量锁,待走出t2同步块后进入无锁状态。第19~29发生重偏向,是偏向锁。 此时经过t2竞争前40个a,已经到了批量撤销的阈值40,而第20~39已经重偏向过t2,不会再次重偏向,所以升级为轻量锁。(偏向锁重偏向一次之后不可再次重偏向。)
的实现 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。 因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。 否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。 ,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。
的实现 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。 因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。 否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。 ,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。
偏向锁和轻量级锁一样也是在JDK 1.6中新增的一种锁,它的目的是为了解决数据在无竞争的时候把同步语句去掉,进一步提高程序的运行性能。 在上一篇中使我们知道轻量级锁是在无数据竞争的时,使用CAS操作去去掉同步的。那么在偏向锁中就是在无数据竞争时把整个同步都去掉,连CAS操作都不需要做了。 偏向锁实际的本质是就是偏向第一个获得它的线程,当这个线程在执行时,如果该锁没有被其他的线程获取,则持有偏向锁的线程将一直不需要进行同步。 下面我们看一下在线程获取偏向锁时,Mark Word都会有哪些变化。 当线程第一次获取锁时,虚拟机会把Mark Word中的标志位修改为01,即偏向模式。 当有其它线程尝试去获取这个锁时,偏向模式也就结束了。下图为偏向锁和轻量级锁转化时对象Mark Word等信息的变化。
当然如果这时有另外一个线程尝试进入偏向锁,即使没有发生竞争,也需要执行 偏向锁撤销操作 轻量锁 当轻量锁通过monitorenter指令获取锁的时候,锁记录肯定会被记录到线程的栈里面去,以表示锁获取操作 此时偏向拥有者会像轻量级锁操作那样,它的堆栈会填入锁记录,然后对象本身的mark word会被更新成指向栈上最老的锁记录,然后线程本身在安全点的阻塞会被释放 > 如果没有被原有的偏向锁持有者持有,会撤销对象重新回到可偏向但是还没有偏向的状态 如果对象当前锁住了是进入轻量锁,如果没有锁住是进入未被锁定的,不可偏向对象 下一个获取锁的操作会与检测对象的mark word,如果对象是可偏向的,并且偏向的所有者是当前那线程,会没有任何额外操作而立马获取锁 这个时候偏向锁的持有者的栈不会初始化锁记录,因为对象偏向的时候,是永远不会检验锁记录的 unlock的时候,会测试mark word的状态,看是否仍然有偏向模式。 ,变成过期,但是可以偏向 5.1 如果 发生垃圾回收,lock会被初始化成可偏向但未偏向的状态(这也可以降低epoch循环使用的影响) 5.2 如果重新被线程获取偏向锁,回到偏向锁获取状态 处于轻量锁状态
今天我们来聊聊Java中的锁!synchronized怎么用?锁是什么?偏向锁是什么?锁如何升级?何为膨胀?自旋锁何解?互斥锁怎么来的?何时要禁用偏向锁和轻量级锁? hashage001 从上面Markword的结构中,可以看出 所有新创建的对象,都是可偏向的(锁标志位为01),但都是未偏向的(是否偏向锁标志位为0)偏向锁当线程执行到临界区(critical section 和epoch值 也就是说,这个锁将自己偏向了当前线程,心里默默地藏着线程id, 在这里,我们就引入了“偏向锁”的概念。 继续执行下面的代码如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。
(重量级锁)的指针 10 GC标记 空 11 偏向锁 线程ID Epoch 对象分代年龄 1 01 无锁 对象的hashCode 对象分代年龄 0 01 “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的 否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 1、偏向锁获取过程: (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。 2、偏向锁的释放: 偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。 解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。
轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。 “偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功 偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。 缺点 同样的,如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁。 不过这个副作用已经小的多。 如果需要,使用参数-XX:-UseBiasedLocking禁止偏向锁优化(默认打开)。 小结 偏向锁、轻量级锁、重量级锁分配和膨胀的详细过程见后。会涉及一些Mark Word与CAS的知识。 偏向锁、轻量级锁、重量级锁适用于不同的并发场景: 1、偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。 2、轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
1 偏向锁的意义 无多线程竞争时,减少不必要的轻量级锁执行路径。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一条线程去多次获得锁,为了让线程获得锁的性能代价更低而引入了偏向锁。 ,即当其他线程尝试获取偏向锁才释放锁 轻量级锁的获取及释放依赖多次的CAS操作,而偏向锁只依赖一次CAS置换ThreadID。 只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 ,偏向锁的撤销会影响效率 偏向锁的重入计数依靠线程栈里Lock Record个数 偏向锁撤销失败,最终会升级为轻量级锁 偏向锁退出时并没有修改Mark Word,也就是没有释放锁 偏向锁相对轻量级锁来说 (轻量级锁在同一线程情况下每次去获取锁,在无锁的状态下,每次都要进行一次CAS操作) 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁 偏向锁的撤销是很复杂,
,对象的所处的状态,决定了markword存储的内容,如下表所示: 状态 标志位 存储内容 未锁定 01 对象哈希码、对象分代年龄 轻量级锁定 00 指向锁记录的指针 膨胀(重量级锁定) 10 执行重量级锁定的指针 JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制; 重量级锁Synchronized Synchronized 偏向锁的实现 偏向锁获取过程: 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。 偏向锁的释放: 偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。 否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
轻量锁就比原来偏向锁麻烦些了 偏向锁只需要借一次钥匙,轻量级锁次次都要借钥匙还要还钥匙,轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可 偏向锁 初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。 轻量级锁的获取主要由两种情况: 当关闭偏向锁功能时; 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。 重量级锁 重量级锁显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。
2、偏向锁 2.1、基本原理 偏向锁是一种对线程友好的锁机制,它的核心思想是通过对线程的识别和追踪,以及对锁的竞争状况进行动态分析,来决定是否启用偏向锁。 3、获取偏向锁 在JDK6开始,HotSpot虚拟机就开启了-XX:UseBiasedLocking参数,默认启用了偏向锁。 ,则将对象头设置成无锁状态;如果线程仍然活着,则需要遍历持有偏向锁的栈,检查是否存在其他对象头和该对象头不一致,如果存在,则需要重新偏向该线程。 4.2、其他线程尝试竞争偏向锁 前面也提到了。当出现另一个线程尝试获取偏向锁的情况下,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。 实际上,当一个对象计算过一致性hash后,就再也无法进入偏向锁状态了。而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。
引入偏向锁的好处偏向锁的好处是并发度很低的情况下,同一个线程获取锁不需要内存拷贝的操作,免去了轻量级锁的在线程栈中建LockRecord,拷贝MarkDown的内容。 另外Hotspot也做了另一项优化,基于锁对象的epoch批量偏移和批量撤销偏移,这样大大降低了偏向锁的CAS和锁撤销带来的损耗。 因为基于epoch批量撤销偏向锁和批量加偏向锁能大幅提升吞吐量,但是并发量特别大的时候性能就没有什么特别的提升了。偏向锁减少CAS操作,降低Cache一致性流量,CAS操作会延迟本地调用。 所以偏向锁比较适用于只有一个线程访问同步块场景。引入轻量级的好处对于绝大部分的锁,在整个同步周期内都是不存在竞争的。如果没有竞争,轻量级锁通过CAS操作成功,避免了使用互斥量的开销。 如果确实存在锁竞争,始终得不到锁竞争的线程使用自旋会消耗CPU,除了互斥量的本身开销外,还额外发生了CAS操作的开销,轻量级锁反而会比传统的重量级锁更慢。
偏向锁状态,锁标志位01,是否偏向是1,存储的是当前占用对象的线程ID。 轻量级锁状态,锁标志位00,存储指向线程栈中锁记录的指针。 重量级锁状态,锁标志位10,存储的就是重量级锁的指针了。 对象从无锁到偏向锁转化的过程 第一步,检测MarkWord是否为可偏向状态,是偏向锁是1,锁标识位是01。 第二步,如果是可偏向状态,测试线程ID是不是当前线程ID。如果是,就直接执行同步代码块。 恢复A线程,将是否为偏向锁状态改为0,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程,继续往下执行同步代码块。 如果不是说明多个线程竞争锁,进入自旋,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态 当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。
今天我们继续来聊聊Java中的锁!解"锁"疑惑:偏向锁为什么不是锁?锁升级又是发生的?何时禁用偏向锁和轻量级锁?synchronized怎么用?锁是什么?偏向锁是什么?锁如何升级?何为膨胀? 锁膨胀如果两个线程都是活跃的,会发生竞争,此时偏向锁就会发生升级,也就是我们常常听到的锁膨胀。 偏向锁会膨胀成轻量级锁(lightweight locking)。 当然,升级成互斥锁之后,锁对象头的Markword内容也是会变化的,其内容如下: bit fields锁标志位指向Mutex的指针10每次检查当前线程是否获得锁,其实就是检查Mutex的值是否为0,不为 从这一点,也可以看出,如果我们的应用场景本身就不适用于偏向锁和轻量级锁,那么我们在程序一开始,就应该禁用掉偏向锁和轻量级锁,直接使用重量级锁,省去无谓的开销。 简单地将上述几个零散的markword变化合在一起,展示在下面: 锁状态bits1bit是否是偏向锁2bit锁标志位无锁状态对象的hashCode001偏向锁线程ID101轻量级锁指向栈中锁记录的指针
注意一点:锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 =0 关闭偏向锁:-XX:-UseBiasedLocking 偏向锁原理:(偏向线程无需每次都要取锁、解锁,就偏向线程来说跟无锁时差别不大) 在实际的应用中经常存在这样一种情况,锁总是一个线程持有 也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。 偏向锁加锁过程: 1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。 否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
三、偏向锁 通俗的讲,偏向锁就是在运行过程中,对象的锁偏向某个线程。 偏向锁的获取流程: (1)查看Mark Word中偏向锁的标识以及锁标志位,若是否偏向锁为1且锁标志位为01,则该锁为可偏向状态。 (4)当前线程通过CAS竞争锁失败的情况下,说明有竞争。当到达全局安全点时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。 偏向锁的释放流程: 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。 偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。
synchronized 在 JVM 中的实现原理 重量级锁对应的锁标志位是 10,存储了指向重量级监视器锁的指针,在 Hotspot中,对象的监视器(monitor)锁对象由 ObjectMonitor 锁的优化 锁的 4 中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高),整个锁的状态从低到高变化的过程被称为所升级。 为什么要引入偏向锁? 偏向锁的升级 当线程 1 访问代码块并获取锁对象时,会在 java 对象头和栈帧中记录偏向的锁的 threadID,因为「偏向锁不会主动释放锁」,因此以后线程1再次获取锁的时候,需要「比较当前线程的 threadID 偏向锁的取消 偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用 -XX:BiasedLockingStartUpDelay=0; 如果不想要偏向锁,那么可以通过 重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转。」 ❝ 注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。
,有的时候它也是很轻的,那么接下来我们就调调,synchronized是怎么被优化的,它跟偏向锁、轻量锁、重量锁又有什么渊源。 “偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功 899685-20161025102843468-151954717.png 偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。 如果此时有线程2来竞争,并且他也尝试cas修改mark word但是失败了,那么线程2会进入自旋状态,如果在自旋状态也没有修改成功,那么轻量锁将膨胀成状态,mark word会被修改成重量锁标记(10) 因此,后来称这种锁为“重量级锁”。 小结 偏向锁、轻量级锁、重量级锁适用于不同的并发场景: 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 偏向锁 引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁 1、偏向锁获取过程: (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。 2、偏向锁的释放: 偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。 解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。