精华内容
下载资源
问答
  • 小黑把小黄带到了卫生间,小黄惊呆了,原来,最近小黑在学习Java并发编程,总是搞不清synchronized膨胀,于是制作了这个智能门帮助自己理解。 小黑也没有使用过这个卫生间,正好小黄要用,就来看看这个门有多...

    小黑和小黄是好朋友,一天,小黑乔迁新居,邀请好朋友小黄来家里庆祝,他们喝了牛奶,吃了巧克力,饭后,两个好朋友在沙发看电视,突然……

    小黑把小黄带到了卫生间,小黄惊呆了,原来,最近小黑在学习Java并发编程,总是搞不清synchronized锁的膨胀,于是制作了这个智能门帮助自己理解。

    小黑也没有使用过这个卫生间,正好小黄要用,就来看看这个门有多智能吧,据说,它能在保证卫生间只有一个人使用的情况下,竞争效率最大化。

    当小黄进入卫生间后,门也开始发生了变化……

    小黄通过CAS将自己的名字写在门上成功,说明卫生间之前是无锁状态,这时,只有小黄一个线程,门上锁类型为偏向门锁。

    恩~,小黑看着门的变化,满意的回到客厅等待小黄,二十分钟后……

    这次小黑没有带小黄去,让小黄自己去卫生间,这个门由于记录了小黄的名字,自动开门,小黄顾不上感叹,飞速冲了进去……

    由于偏向门锁是在只有一个人进出卫生间时生效,所以,当小黄再次进入卫生间时,仅仅对比了一下门上记录的名字和小黄的名字,发现小黄是持有偏向门锁的人,放行。

    一个小时后,小黄要走了,小黑去送别,但是……

    小黑送走了小黄,但是自己的肚子也疼了起来,他赶到了卫生间,第一次居然打不开门……

    这里小黑用CAS将自己的名字写到门上失败,因为之前小黄曾经在门上记录了名字。

    小黑想起,刚才小黄用了卫生间,但是现在小黄回家了,于是小黑把门上的名字改为了自己,冲了进去……

    虽然发生了竞争,但是小黑在检查后发现,小黄不会再次进入卫生间(即锁偏向的线程已经消亡),所以,现在还是相当于只有小黑进出卫生间,于是,他将门上的名字改为了自己,而锁的类型没有变化。

    解决完大事之后,小黑一身轻松,正躺在沙发看电视,只听见大门,咚咚咚~,开门后……

    哦,好的,小黑应了一声,转身带小小黄去了卫生间……

    同样,小小黄也遇到了和小黑同样的问题,第一次打不开门……

    小小黄就没有小黑那么幸运了,由于小黑是主人,他还会再用卫生间,也就是说,此时发生了多个人进出一个卫生间(不同时竞争),锁类型变为了轻量级门锁。

    于是,他进入门之后,门又发生了一些变化……

    这时,客厅里的小黑坐不住了……

    小黑这才想起来,牛奶和巧克力不能同食,会引起腹泻,来不及想这些了,小黑冲到卫生间门口,却发现小小黄还在里面……

    小黑想,我就等五分钟,还不出来的话我就回卧室等着。

    这里,由于门锁为轻量级,所以小黑选择在门口等待(自旋)。

    五分钟后,小小黄还没出来,小黑无奈回了卧室,他没有注意的是,门上的字,也悄悄发生了变化……

    门的变化只能从偏向→轻量级→重量级变化,不能逆向,目的是为了保证效率。因为既然能膨胀成重量级门锁,说明肯定有多个人同时竞争卫生间或者在门外等待的时间比使用卫生间的时间还要长,所以,这样做是为了避免不必要的消耗(CAS自旋实际上是CPU空转)。

    人物的名字相当于线程id,门上所写内容均在Mark Word(对象头)中,卫生间也就是我们所说的同步代码块。

     



    作者:架构师修练手册
    链接:https://www.jianshu.com/p/8bd25f680257
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • JAVA锁膨胀过程

    2020-11-14 15:31:34
    Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观,在操作同步资源的时候直接先加锁。 加锁可以使一段代码在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是...

    一、背景

    在 Java 语言中,使用 Synchronized 是能够实现线程同步的,即加锁。并且实现的是悲观锁,在操作同步资源的时候直接先加锁。

    加锁可以使一段代码在同一时间只有一个线程可以访问,在增加安全性的同时,牺牲掉的是程序的执行性能,所以为了在一定程度上减少获得锁和释放锁带来的性能消耗,在 jdk6 之后便引入了“偏向锁”和“轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级,此过程为不可逆。所以 synchronized 锁膨胀过程其实就是

    无锁 → 偏向锁 → 轻量级锁 → 重量级锁

    的一个过程。
    在这里插入图片描述
    在使用 synchronized 来同步代码块的时候,编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。当执行到 monitorenter 指令时,将会尝试获取对象所对应的monitor 的所有权,即尝试获得对象的锁。而 synchronized 用的锁是存放在 Java对象头 中的。

    所以引出了两个关键词:“Java 对象头” 和 “Monitor”

    二、Java 对象头和 Monitor

    1、Java 对象头

    我们以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。

    Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。

    Class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    2、Monitor

    Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

    Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

    三、锁

    无锁

    无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

    无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

    偏向锁

    引入偏向锁的主要目的是:为了在无多线程竞争的情况下尽量减少不必须要的轻量级锁执行路径。其实在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁就可以减少很多不必要的性能开销和上下文切换。

    偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

    当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

    偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

    关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

    偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

    轻量级锁

    引入轻量级锁的主要目的是:在多线程竞争不激烈的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。

    所以偏向锁是认为环境中不存在竞争情况,而轻量级锁则是认为环境中不存在竞争或者竞争不激烈,所以轻量级锁一般都只会有少数几个线程竞争锁对象,其他线程只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

    重量级锁

    重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

    重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

    简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。

    下面说下膨胀过程,直接上图:
    在这里插入图片描述

    synchronized 用的锁是存储在 Java 对象头里的,下图是锁状态变化的情况,在分析 synchronized 锁升级需要对照这图:

    在这里插入图片描述
    一个锁对象刚刚开始创建的时候,没有任何线程来访问它,它是可偏向的,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问他的时候,它会偏向这个线程。此时线程状态为无锁状态,锁标志位为 01,此时 Mark Word 如下图:
    在这里插入图片描述

    当一个线程(线程 A)来获取锁的时,会首先检查所标志位,此时锁标志位为 01,然后检查是否为偏向锁,此时不为偏向锁,所以当前线程会修改对象头状态为偏向锁,同时将对象头中的 ThreadID 改成自己的 Thread ID,此时 Mark Word 如下图
    在这里插入图片描述

    如果再有一个线程(线程 B)过来,此时锁状态为偏向锁,该线程会检查 Mark Word 中记录的线程 ID 是否为自己的线程 ID,如果是,则获取偏向锁,执行同步代码块。如果不是,则利用 CAS 尝试替换 Mark Word 中的 Thread ID,成功,表示该线程(线程 B)获取偏向锁,执行同步代码块,此时 Mark Word 如下图:
    在这里插入图片描述

    如果失败,则表明当前环境存在锁竞争情况,则执行偏向锁的撤销工作(这里有一点需要注意的是:偏向锁的释放并不是主动,而是被动的,什么意思呢?就是说持有偏向锁的线程执行完同步代码后不会主动释放偏向锁,而是等待其他线程来竞争才会释放锁)。撤销偏向锁的操作需要等到全局安全点才会执行,然后暂停持有偏向锁的线程,同时检查该线程的状态,如果该线程不处于活动状态或者已经退出同步代码块,则设置为无锁状态(线程 ID 为空,是否为偏向锁为 0 ,锁标志位为01)重新偏向,同时恢复该线程。若该线程活着,则会遍历该线程栈帧中的锁记录,检查锁记录的使用情况,如果仍然需要持有偏向锁,则撤销偏向锁,升级为轻量级锁。

    在升级为轻量级锁之前,持有偏向锁的线程(线程 A)是暂停的,JVM 首先会在原持有偏向锁的线程(线程 A)的栈中创建一个名为锁记录的空间(Lock Record),用于存放锁对象目前的 Mark Word 的拷贝,然后拷贝对象头中的 Mark Word 到原持有偏向锁的线程(线程 A)的锁记录中(官方称之为 Displaced Mark Word ),这时线程 A 获取轻量级锁,此时 Mark Word 的锁标志位为 00,指向锁记录的指针指向线程 A 的锁记录地址,如下图:
    在这里插入图片描述
    当原持有偏向锁的线程(线程 A)获取轻量级锁后,JVM 唤醒线程 A,线程 A 执行同步代码块,执行完成后,开始轻量级锁的释放过程。

    对于其他线程而言,也会在栈帧中建立锁记录,存储锁对象目前的 Mark Word 的拷贝。JVM 利用 CAS 操作尝试将锁对象的 Mark Word 更正指向当前线程的 Lock Record,如果成功,表明竞争到锁,则执行同步代码块,如果失败,那么线程尝试使用自旋的方式来等待持有轻量级锁的线程释放锁。当然,它不会一直自旋下去,因为自旋的过程也会消耗 CPU,而是自旋一定的次数,如果自旋了一定次数后还是失败,则升级为重量级锁,阻塞所有未获取锁的线程,等待释放锁后唤醒。

    轻量级锁的释放,会使用 CAS 操作将 Displaced Mark Word 替换到对象头中,成功,则表示没有发生竞争,直接释放。如果失败,表明锁对象存在竞争关系,这时会轻量级锁会升级为重量级锁,然后释放锁,唤醒被挂起的线程,开始新一轮锁竞争,注意这个时候的锁是重量级锁。

    四、自旋

    引入自旋这一规则的原因其实也很简单,因为阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。并且在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,这部分操作的开销其实是得不偿失的。

    所以,在物理机器有多个处理器的情况下,当两个或两个以上的线程同时并行执行时,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋。如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。

    自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。

    所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

    自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

    自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    展开全文
  • JAVA锁升级(锁膨胀)的过程 文章目录JAVA锁升级(锁膨胀)的过程背景概念无锁偏向锁轻量锁(自旋锁)重量级锁锁的状态主要表现在对象头的MarkWord中 背景 在jdk获取锁的前期,需要jvm向内核申请,需要计算机内核...

    JAVA锁升级(锁膨胀)的过程

    1.背景

    在jdk获取锁的前期,需要jvm向内核申请,需要计算机内核参与,因此获取锁和释放锁的成本大大提高;在jdk1.6之后有所优化,但是为了提高锁的效率,锁升级还是大大的有必要。锁只能升级不能降级,升级的策略是为了提高锁的效率。

    jdk1.6之后锁分四种状态:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

    2.概念

    2.1.无锁

    程序不会有锁的竞争,在程序设计不需要考虑锁的情况下为无锁。

    2.2.偏向锁

    在程序设计需要考虑锁的情况下,在运行过程中,只有一个线程访问代码块,而且还未产生多线程竞争的条件下,程序会添加一个偏向锁,标识代码块已经有线程访问,当同一个线程再次访问时,可以跳过加锁操作,由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

    如果运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作。

    2.3.轻量锁(自旋锁)

    在发生锁竞争的情况下,未获得锁的线程发生自旋,直到获取到锁为止。但是长时间的自旋操作非常影响性能,如果达到了一下两个条件,则会再次升级为重量级锁。

    • 自旋时间超过某个阈值
    • 自旋线程数超过某个阈值

    2.4.重量级锁

    重量级锁会将自己挂起,加入等待队列,释放cpu资源,等待其他线程唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

    优点 缺点 适用场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
    重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

    3.锁的状态主要表现在对象头的MarkWord中

    锁状态 标志位
    无锁 001
    偏向锁 101
    轻量锁 00
    重量锁 10
    展开全文
  • 网上看到一个文章讲解 JAVA 中的,看的过程中发现了很多不理解的地方,自行百度以后觉得很有用,遂记录一下。自旋自旋其实就是在拿时发现已经有线程拿了,自己如果去拿会阻塞自己,这个时候会选择进行一次...

    网上看到一个文章讲解 JAVA 中的锁,看的过程中发现了很多不理解的地方,自行百度以后觉得很有用,遂记录一下。

    自旋锁

    自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。这个问题是基于一个现实考量的:很多拿了锁的线程会很快释放锁。因为一般敏感的操作不会很多。当然这个是一个不能完全确定的情况,只能说总体上是一种优化。

    然后是基于这种做法的一个优化:自适应自旋锁。也就是说,第一次设置最多自旋 10 次,结果在自旋的过程中成功获得了锁,那么下一次就可以设置成最多自旋 20 次。道理是:一个锁如果能够在自旋的过程中被释放说明很有可能下一次也会发生这种事。那么就更要给这个锁某种“便利”方便其不阻塞得锁(毕竟快了很多)。同样如果多次尝试的结果是完全不能自旋等到其释放锁,那么就说明很有可能这个临界区里面的操作比较耗时间。就减小自旋的次数,因为其可能性太小了。

    锁粗化

    试想有一个循环,循环里面是一些敏感操作,有的人就在循环里面写上了 synchronized 关键字。这样确实没错不过效率也许会很低,因为其频繁地拿锁释放锁。要知道锁的取得(假如只考虑重量级 MutexLock )是需要操作系统调用的,从用户态进入内核态,开销很大。于是针对这种情况也许虚拟机发现了之后会适当扩大加锁的范围(所以叫锁粗化)以避免频繁的拿锁释放锁的过程。

    锁消除

    通过逃逸分析发现其实根本就没有别的线程产生竞争的可能(别的线程没有临界量的引用),而“自作多情”地给自己加上了锁。有可能虚拟机会直接去掉这个锁。

    偏向锁和轻量级锁

    这两个锁既是一种优化策略,也是一种膨胀过程所以一起说。首先它们的关系是:最高效的是偏向锁,尽量使用偏向锁,如果不能(发生了竞争)就膨胀为轻量级锁,这样优化的效率不如原来高不过还是一种优化(对比重量级锁而言)。所以整个过程是尽可能地优化。

    偏向锁

    HotSpot 的研究人员发现大多数情况下虽然加了锁,但是没有竞争的发生,甚至是同一个线程反复获得这个锁。那么偏向锁就为了针对这种情况。偏向锁的目的是为了在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不用做。

    举个例子,一个仓库管理员管着钥匙,然而每一次都是老王去借,仓库管理员于是就认识了老王,直接和他说,“行,你直接拿就是不用填表格了我记得你”。

    偏向锁的获取

    讲一下偏向锁的具体过程。首先JVM要设置为可用偏向锁。然后当一个线程访问同步块并获取锁时,会把对象头中的标志位设置为 “01” ,即偏向模式,同时使用 CAS 操作,在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。

    如果测试失败,那么分为两种情况。对象的偏向锁标志位为 0 ,当前不是偏向锁,说明发生了竞争,已经膨胀为轻量级锁,这时使用 CAS 操作尝试竞争锁(这个操作具体是轻量级锁的获得锁的过程下面讲)。

    对象的偏向锁标志位为 1 ,说明还是偏向锁,但请求的线程不是原来那个了。这时只需要使用 CAS 尝试把对象头偏向锁从原来那个线程指向目前请求锁的线程。这种情况举个例子就是老王准备退休了,他儿子接替他来拿钥匙,于是仓库管理员认识了他儿子,他儿子每次来也不用登记注册了。

    这里解释一下 CAS :

    CAS 操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用 CAS(compare and swap) 又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

    CAS 比较交换的过程可以通俗的理解为 CAS(V,O,N) ,包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当 V 和 O 相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值 O 就是目前来说最新的值了,自然而然可以将新值N赋值给 V 。反之, V 和 O 不相同,表明该值已经被其他线程改过了则该旧值 O 不是最新版本的值了,所以不能将新值 N 赋给 V ,返回 V 即可。当多个线程使用 CAS 操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

    如果这个 CAS 失败了呢?首先必须明确这个 CAS 为什么会失败,也就是说发生了竞争,有别的线程和它抢锁并且抢赢了,那么这个情况下,它就会要求撤销偏向锁(因为发生了竞争)。

    偏向锁的撤销

    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

    偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,检查这个线程是否是个活动线程,如果不是,那么好,你拿了锁但是没在干事,锁还记录着你,那么直接把对象头设置为无锁状态重新来过。如果还是活动线程,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

    下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。

    轻量级锁

    轻量级锁加锁

    轻量锁是偏向锁膨胀后的产物,线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word 。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为”10”, Mark Word 中存储的就是指向重量级(互斥量)的指针。

    轻量级锁解锁

    轻量级解锁时,会使用原子的 CAS 操作来将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

    下图是两个线程同时争夺锁,导致锁膨胀的流程图。

    因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

    引用别人的见解

    偏向锁,轻量级锁都是乐观锁,重量级锁是悲观锁。

    一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用 CAS 操作,并将对象头中的 ThreadID 改成自己的 ID ,之后再次访问这个对象时,只需要对比 ID ,不需要再使用 CAS 在进行操作。

    一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。如果对象未锁定,那么将该对象标记为未被锁定的,不可偏向对象。

    轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止 CPU 空转。

    转载请标注原文链接

    展开全文
  • 1,自旋自选其实就是在拿时发现已经有线程拿了,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放。这个问题是基于一个现实考量的:很多拿了的...
  • 阿里的人问什么是锁膨胀,答不上来,回来做了总结:关于锁的膨胀,synchronized的原理参考:深入分析Synchronized原理(阿里面试题)首先说一下锁的优化策略。1,自旋锁自旋锁其实就是在拿锁时发现已经有线程拿了锁,...
  • — 扫描二维码 —加入架构集结群 对技术感兴趣的同学可进群(备注:Java)首先说一下的优化策略。1,自旋自选其实就是在拿时发现已经有线程拿了,自己如果去拿会...
  • } } 看下结果 自旋 自旋一段时间,可以理解为空转,时间很短,具体时间需要看jvm源码,如果在自旋时间内拿到了,就不再膨胀,如果还是拿不到,则膨胀为重量,如下 public static void main(String[] args) throws...
  • JAVA锁优化和膨胀过程

    2019-04-18 11:38:12
    高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁削除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级...
  • 1、优化 在JDK6之前,通过synchronized来实现同步效率是很低的,被synchronized包裹的代码块经过javac编译后,会在代码块前后加上monitorenter和monitorexit字节码指令,被synchronized修饰的方法则会被加上ACC_...
  • 前言上一篇分析了优化后的synchronized...这句话看起来很简单,但实际上synhronized的膨胀过程是非常复杂的,有许多场景和细节需要考虑,本篇就对其进行详细分析。正文先来看一个案例代码:这里创建了三个线程t1、t2...
  • Java锁膨胀过程和优化

    千次阅读 2017-05-08 22:50:36
    首先说一下的优化策略。 1,自旋 自选其实就是在拿时发现已经有线程拿了,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放。这个问题是基于一...

空空如也

空空如也

1 2 3 4 5 6
收藏数 107
精华内容 42
关键字:

java锁膨胀过程

java 订阅