synchronized锁升级 博主 默语带您 Go to New World. 锁的优化:从无锁到重量级锁 Java 在 JDK 6 之后通过 偏向锁、轻量级锁 和 重量级锁 提高了锁的性能。这些优化基于对象头中的 MarkWord 字段实现。 对象头结构 在 32 位虚拟机中,MarkWord 包括: 哈希码(25 位) 分代年龄(4 位) 锁标志(2 位) 偏向标志(1 位) 无锁状态 初始状态,未被任何线程持有。 偏向锁的升级 如果锁竞争发生,偏向锁可能升级为轻量级锁。 轻量级锁 特点:通过自旋等待来实现,适合短时间内无大量竞争的场景。 撤销:轻量级锁会在使用后主动撤销,将 MarkWord 恢复原值。 升级:若自旋超出限定次数或有大量线程竞争,升级为重量级锁。
锁的优化 锁的 4 中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高),整个锁的状态从低到高变化的过程被称为所升级。 为什么要引入偏向锁? (线程 1)的栈帧信息,如果还是需要继续持有这个锁对象」,那么暂停当前线程 1,撤销偏向锁,升级为轻量级锁,如果线程 1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。 轻量级锁什么时候升级为重量级锁? 重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转。」 ❝ 注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。 一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
0x01:偏向锁 偏向第一个拿到锁的线程。 即第一个拿到锁的线程,锁会在对象头 Mark Word 中通过 CAS 记录该线程 ID,该线程以后每次拿锁时都不需要进行 CAS(指轻量级锁)。 如果该线程正在执行同步代码块时有其他线程在竞争(指其他线程尝试 CAS 让 Mark Word 设置自己的线程 ID),会被升级为轻量级锁。 如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。 升级为重量级锁时会在堆中创建 monitor 对象,并将 Mark Word 指向该 monitor 对象。 图片来自:死磕Synchronized底层实现--重量级锁 锁升级的流程图 ? 图片来自:Java Synchronised机制 0x05:锁降级 Hotspot 在 1.8 开始有了锁降级。
我将独自升级!-- 锁升级 大家好,我是小高先生。在经过对锁的基础知识和对象头概念的学习之后,相信各位已经对锁机制有了初步的了解。在之前的文章中,我有提到过关于锁升级的概念。 这样的权衡和折中,也是锁机制设计的智慧所在。因此,为了找到一个平衡点,Java中的锁机制引入了一个过程,即“锁升级”。 如果争夺成功,Mark Word会记录新的线程ID,但偏向锁不会升级。如果争夺失败,那竞争会依旧存在,此时偏向锁升级为轻量级锁,以便更公平的处理多线程之间的竞争关系。 自旋一定次数后线程还没获取到锁,轻量级锁就会升级为重量级锁,,因为自旋会消耗CPU。在Java 6之前,默认情况下自旋次数超过10次升级,或者自选线程数超过CPU核数一般,了解即可。 如果对象处于偏向状态,此时又调用了hashCode(),偏向锁会被撤销,升级为重量级锁。
锁的优化 锁的 4 中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高),整个锁的状态从低到高变化的过程被称为所升级。 为什么要引入偏向锁? (线程 1)的栈帧信息,如果还是需要继续持有这个锁对象」,那么暂停当前线程 1,撤销偏向锁,升级为轻量级锁,如果线程 1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。 轻量级锁什么时候升级为重量级锁? 重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转。」 ❝ 注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。 一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略 2、Synchronized的性能变化 java5以前,只有Synchronized,这个是操作系统级别的重量级操作 ,引入了轻量级锁和偏向锁 2、为什么每一个对象都可以成为一个锁? 锁种类及升级步骤 1、多线程访问情况,3种 1)只有一个线程来访问,有且唯一Only One 2)有2个线程A、B来交替访问 3)竞争激烈,多个线程来访问 2、升级流程 synchronized用的锁是存在 2、偏向锁的撤销 当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁 竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。 升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁 假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
64操作系统下,Mark Word的长度是64,在加Klass Word(32位),一共是96位,其实对象头长什么样其实不是本文的重点,本文的重点是验证锁升级的过程,所以我们只需要关注对象头中Mark 锁升级的过程 锁状态 25bit 4bit 1bit 2bit 23bit 2bit 是否偏向锁 锁标志位 1 无锁 对象的HashCode 分代年龄 0 01 2 无锁 对象的HashCode 上面表格中2->3的过程。 ,偏向于线程1 线程2上锁,上锁后,状态为00,轻量级锁状态 线程2释放锁后,状态为001,此时为不可偏向的无锁状态。 ) .start(); } } class Dog { int age; } 如上图所示,锁初始状态为101,可偏向无锁状态 当线程1在使用锁,而线程2去上锁的时候,状态已经变为
锁升级 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁。 并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,这四种锁的级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级锁。如下图所示: ? 轻量级锁 轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。 轻量级锁的获取主要由两种情况: 当关闭偏向锁功能时; 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。 如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。
锁升级 首先执行下面这一段代码: Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable : 1.刚刚new出对象开始时未上锁 2.第一次对其加锁被称之为:偏向锁 3.接下来升级为轻量级锁:无锁或者自旋锁 4.最终升级为:重量级锁 理解自旋锁和无所:自旋锁:假如有一哥们在蹲坑,你在旁边转圈等待 你可以叫他自旋锁,也相当于不是锁,所以叫做:无锁 详细讲解锁升级过程: 无状态锁:new Object的时候。 :只要被访问的资源处于竞争状态时,自动升级为自旋锁,多线程同时并发访问同一个为资源,此时每条线程在自己的线程栈当中生成一个Lock Record对象,并且开始以CAS的自旋方式去抢占被访问的资源,该资源会记录轻量级锁的指针 ,也就是说会不断的比较被抢占资源的值是否与自己的指针是否相等,如果相等,那么就修改该指针 重量级锁:当自旋锁长期处于自旋状态,太过于消耗CPU资源,于是升级为重量级锁,重量级锁是必须要由操作系统匹配的(
三、锁升级全流程核心原理 synchronized的锁升级是JVM为了减少同步开销而做的自适应优化,核心逻辑是:根据竞争激烈程度,从低开销锁逐步升级到高开销锁,锁的膨胀过程在持有期间是单向的,不可降级, 锁升级全流程总览 3.1 第一阶段:无锁状态 无锁状态分为两种子状态,是锁升级的起点: 无锁可偏向状态:开启偏向锁的前提下,对象刚创建,未调用过System.identityHashCode(),Mark 2:刚创建,可偏向状态 ====="); printObjectHeader(LOCK_OBJECT_2); log.info("===== 锁对象2:调用identityHashCode _2); log.info("===== 锁对象2:获取锁,无法进入偏向锁,直接进入轻量级锁 ====="); synchronized (LOCK_OBJECT_2) 轻量级锁:出现多线程交替竞争锁时,偏向锁被撤销,升级为轻量级锁。
锁的存储 在多线程并发编程中,synchronized一般我们认为是重量级锁,但是随着JDK1.6的优化之后,在一些情况下它就不显得那么重量级了,因为在JDK1.6中为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁跟轻量级锁 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。 这里其实就是一种锁的升级,毕竟synchronized锁了代码是因为防止多个线程并发的时候对数据造成不安全,但是如果只有线程访问,那synchronized就太消耗性能了,所以做了偏向锁进行升级。。 轻量级锁 当偏向锁已经不足够使用的时候,会再次升级为轻量级锁,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。 轻量级锁再加锁过程中,会使用到了自旋锁,而自旋锁就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是阻塞该线程,直到前面的线程释放锁了之后,这个线程就可以马上获得锁。
主线程进入,锁升级为 00(轻量级锁) OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 线程 1 和线程 2 同时进入 ,存在竞争 ,升级位重量级锁 10 OFFSET SIZE TYPE DESCRIPTION VALUE 运行 hashcode 后,锁状态升级位无锁不可偏向 ,原本存放线程 ID 的位置被 hashcode 覆盖 OFFSET SIZE TYPE DESCRIPTION 线程 1 进入,无锁竞争,锁升级为轻量锁 000 OFFSET SIZE TYPE DESCRIPTION VALUE 0 线程 2 进入,存在锁竞争,锁升级为轻量锁 010 OFFSET SIZE TYPE DESCRIPTION VALUE 0
: 确认锁膨胀事件 Object-->>Thread1: Object允许请求 Note over Object, JVM: Thread 2持有轻量级锁 Thread2-> 然后当线程2试图获取锁时,偏向锁被撤销,变回无锁状态。再次由线程1获取时,锁被升级为轻量级锁。最后,当线程2再次试图获取锁时,锁被升级为重量级锁。 锁的升级一般发生在下述条件(在锁升级前会先进行锁的膨胀): 无锁到偏向锁: 如果一个线程第一次访问一个synchronized块,JVM将会在对象头上记录这个线程ID,然后线程将持有偏向锁。 ID(54位) + Epoch(2位) + 是否是偏向锁(1位) + 锁标志位(2位) 轻量级锁状态: 指向栈中锁记录的指针(62位) + 锁标志位(2位) 重量级锁状态: 指向重量级锁(即管程或Monitor 重量级锁 重新回到对象头中的源码,其实在轻量级锁中也提及了重量级锁在什么时候会进行升级(如果轻量级锁的CAS操作失败,则由JVM选择是否升级为重量级锁)开发者需要重点关注的是inflate(current
,锁可以从 偏向锁 升级到 轻量级锁,再升级到 重量级锁但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级Java 对象结构对象头Mark WordMark Word(标记字)主要用来表示对象的线程锁状态 ,2 位锁标志 (01) 组成偏向状态由 23 位线程标识,2 位时间戳,4 位 GC 分代年龄,1 位偏向锁标识,2 位锁标识 (01) 组成轻量级锁状态由 30 位指针 (指向锁记录) 2 位锁标识 (00) 组成重量级锁状态由 30 位指针指向重量级锁的 monitor 2 位锁标识 (10) 组成Mared for GC待 GC 回收状态,只有最后 2 位锁标识 (11) 有效图片偏向锁偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径轻量级锁的获取及释放依赖多次 ,则表示有竞争当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁 升级为 轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码(5)执行同步代码偏向锁的释放偏向锁只有遇到其他线程尝试竞争偏向锁时 的优化但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程这个时候就需要通过 -XX:-UseBiasedLocking 来禁用偏向锁几种锁的对比图片锁升级代码演示创建一个
---- ---- 无索引行锁升级为表锁演示 表结构 mysql> desc country; +-------------+--------------+------+-----+---------+ 现在使用没有建立索引的字段进行操作,观察其结果 ---- 操作演示 session1 session2 begin 模拟开启事务 update country set countryname = ‘CCC anotherline’ ; ---- 一直被阻塞 ,直到超时 1205 - Lock wait timeout exceeded; try restarting transaction 我们知道锁主要是加在索引上 ,如果对非索引字段更新,行锁可能会变表锁 , 从上面的测试中也可以验证这个观点,第二个 ---- 结论 InnoDB的行锁是针对索引加的锁,不是针对记录加的锁 ,并且该索引不能失效,否则会从行锁升级为表锁
但是在JavaSE1.6的时候,对synchronized进行了优化,引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程,减少了获取锁和释放锁的性能消耗,有些情况下它也就不那么重了。 synchronized锁升级 synchronized锁优化的背景: 用锁能实现数据的安全性,但是会带来性能下降。 无锁能够基于线程并行提升程序性能,但是会带来线程安全性下降。 synchronized锁:根据对象头的mark word 锁标志位来确定当前属于哪一种锁。 为什么会存在锁升级现象? 一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。 升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁 假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
,这种情况就会出现死锁 试想一下:读锁与读锁之间不存在互斥的问题,读锁跟写锁互斥,互斥就是为了防止读数据期间临界区资源不被 修改,如果获取读锁后,同时其他线程也获取到了读锁,然后该线程试能成功获取写锁, **/ private void testUpgrade(){ //获取读锁 rl.lock(); //获取读锁 wl.lock(); wl.unlock() ; rl.unlock(); log.info("结束"); } /** 下面看下获取读锁后再获取写锁死锁的地方 **/ public final void acquire(int arg) { //tryAcquire失败后会进入acquireQueued方法,acquireQueued里面会不断调用tryAcquire方法试图获取写锁,但本文所说的情况下是不可能获得写锁的 * 2. If count would saturate, fail.
} 锁升级 首先过一下synchronized锁升级的过程 1.偏向锁 当只有一个线程获得了锁,锁就进入偏向模式,MarkWord标识偏向状态,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程 2.轻量级锁 当有其它线程要获取锁,竞争不是很激烈,锁进入轻量级锁,MarkWord标识轻量级状态,此时等待锁的线程开始自旋,即空循环等待锁释放,此过程不释放cpu。 原始的synchronized是直接使用重量级锁,才会导致性能很低,加入锁升级才使得synchronized性能获得很大提升。 2.轻量级锁 如果B团队想使用会议室,此时A还占用着会议室(写字板上记录偏向 A团队),此时出现了竞争,写字板上修改为轻量竞争,B团队哪也不去,就在会议室外原地打转(自旋)等着,因为公司大部分会议时间都很短 这就是轻量级锁,偏向锁出现了竞争会升级为轻量级锁,因为大部分线程占用锁的时间不会特别长,所以等待线程刚开始不需要挂起,只需要通过空转自旋等待,一般很快就会获取到锁,比过程一直占用着cpu。
什么是锁升级? 锁升级是指将当前锁的粒度降低,如一把行锁升级唯一把页锁,或者将页锁升级为表锁,如果在数据库设计中认为锁是一中稀有资源,哪么就会频繁有锁升级的现象 发生锁升级的现象 当一条SQL语句对一个对象上持有的锁数量超锁了阈值 ,默认这个阈值为5000,但是对于不同对象不会发生锁升级 锁资源占用的内存超过激活内存的百分之40 就会发生锁升级 但是!!!!! innoDB 引擎不存在锁升级的问题,因为其不是根据每个记录来产生啊行锁的,是根据每个事务访问的每个页对锁进行管理的。 ? 其实吧,这个根据页进行加锁我没搞懂,X,S锁作何解释,难道不是当一条SQL语句加的锁范围大了 在next-keys-locks 的加锁算法下导致全页被锁住 或全表被锁住。 我感觉这玩意也是锁升级啊。
syncronized锁升级 很多文章已经介绍过JDK早期(1.6之前不包括1.6),syncronized是重量级锁。 ,所以在1.6版本进行了大幅改造升级,于是就出现了现在常通说的锁升级或锁膨胀的概念,整体思路就是能不打扰操作系统大哥就不打扰大哥,能在用户态解决的就不经过内核。 位和64位的虚拟机(未开启压缩指针.jvm配置参数:UseCompressedOops,compressed--压缩、oop--对象指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位, 结果可以看出:所有线程结束后已经不存在竞争时并不会变为无锁状态,也就是说锁只能升级,不能降级,竞争比较严重时升级为重量级锁,偏向锁和轻量级锁在用户态维护不需要经过内核态,重量级锁需要切换到内核态(os) 最后留下几个问题思考 简述锁升级过程? 自旋锁什么时候升级为重量级锁? 为什么有自旋锁还需要重量级锁? 偏向锁是否一定比自旋锁效率高?