java八股文面试[多线程]——synchronized锁升级详细流程

这篇具有很好参考价值的文章主要介绍了java八股文面试[多线程]——synchronized锁升级详细流程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

偏向锁

偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁

在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。如果并发数较大同时同步代码块执行时间较长,则被多个线程同时访问的概率就很大,就可以使用参数-XX:-UseBiasedLocking来禁止偏向锁(但这是个JVM参数,不能针对某个对象锁来单独设置)。

引入偏向锁主要目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须小于节省下来的CAS原子指令的性能消耗)。

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

那么偏向锁是如何来减少不必要的CAS操作呢?首先我们看下无竞争下锁存在什么问题:

现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

SMP(对称多处理器)架构

其意思是 所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。

而CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

例如:Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量

而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。

Cache一致性:

上面提到Cache一致性,其实是有协议支持的,现在通用的协议是MESI(最早由Intel开始支持),具体参考:http://en.wikipedia.org/wiki/MESI_protocol。

Cache一致性流量的例外情况:

其实也不是所有的CAS都会导致总线风暴,这跟Cache一致性协议有关,具体参考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot

NUMA(Non Uniform Memory Access Achitecture)架构:

与SMP对应还有非对称多处理器架构,现在主要应用在一些高端处理器上,主要特点是没有总线,没有公用主存,每个Core有自己的内存,针对这种结构此处不做讨论。

所以,当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  3. 如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块;

偏向锁的释放采用了 一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点上是没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程;
  2. 判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以允许其余线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头Mark Word,升级为轻量级锁状态(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式;

注意:此处将 当前线程挂起再恢复的过程中并没有发生锁的转移,仍然在当前线程手中,只是穿插了个 “将对象头中的线程ID变更为指向锁记录地址的指针” 这么个事。

偏向锁的获取和释放过程

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

轻量级锁

引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。此时线程堆栈与对象头的状态如下图所示:

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

                                轻量级锁CAS操作之前线程堆栈与对象的状态

  • 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;

  • 拷贝成功后,虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。(Lock Word属于线程,对象中的Lock Word是指向线程Lock Word的指针,线程Lock Word 记录对象的Object mark word, 相当于 对象和线程相互关联

  • 如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,此时线程堆栈与对象头的状态如下图所示:

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

                                轻量级锁CAS操作之后线程堆栈与对象的状态

  • 如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行,若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
  2. 如果替换成功,整个同步过程就完成了,恢复到无锁状态(01);
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;

对于轻量级锁,其性能提升的依据是 “对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

                                                轻量级锁的获取和释放过程

  1. 为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?

    因为在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。

  2. 为什么会尝试CAS不成功以及什么情况下会不成功?

    CAS本身是不带锁机制的,其是通过比较而来。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操作前的对象HashCode了,所以CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。

    然后线程B进行CAS自旋,等待对象头的锁标识重新变回无锁状态或对象头内容等于对象HashCode(因为这是线程B做CAS操作前的值),这也就意味着线程A执行结束(参见后面轻量级锁的撤销,只有线程A执行完毕撤销锁了才会重置对象头),此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。如果线程A的执行时间较长,线程B经过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待重新调度。

此处,如何理解“轻量级”?“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁。

 重量级锁

Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

重量级锁、轻量级锁和偏向锁之间转换

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

                                重量级锁、轻量级锁和偏向锁之间转换

java八股文面试[多线程]——synchronized锁升级详细流程,java八股文,java,面试,开发语言

                        Synchronized偏向锁、轻量级锁及重量级锁转换流程

知识来源:

https://www.cnblogs.com/aspirant/p/11470858.html文章来源地址https://www.toymoban.com/news/detail-687540.html

到了这里,关于java八股文面试[多线程]——synchronized锁升级详细流程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • java八股文面试[多线程]——线程池拒绝策略

    四种线程池拒绝策略(handler)           当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution (Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略: Abort Policy(默认

    2024年02月10日
    浏览(47)
  • java八股文面试[多线程]——自旋锁

    优点: 1.  自旋锁尽可能的减少线程的阻塞, 这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗  ,这些操作会导致线程发生两次上下文切换! 2. 非自旋锁在获取不到锁的时候会进入阻

    2024年02月10日
    浏览(51)
  • java八股文面试[多线程]——并发三大特性 原子 可见 顺序

        AutomicInteger :  volatile + CAS 总线LOCK  MESI 两个协议 TODO volatile的可见性和禁止重排序是怎么实现的: DCL场景:  new操作会在字节码层面生成两个步骤: 分配内存、调用构造器 然后把引用赋值给singleton 不加volatile则会发生指令重排,可能得到不完整的对象 知识来源: 【并

    2024年02月11日
    浏览(53)
  • java八股文面试[多线程]——两个线程交替打印1-100之间的数字

    一份代码,两个线程,使用synchronize实现: 重写run()方法,将输出1到100之间整数的代码写到同步方法里。 线程1进入到同步方法,输出一个整数后,阻塞并释放锁。 线程2进入到同步方法,唤醒线程1,输出整数后,阻塞并释放锁。 线程1和线程2重复第3步,直到输出所有的整数

    2024年02月11日
    浏览(45)
  • java八股文面试[多线程]——主内存和工作内存的关系

    JAVA内存模型(JMM) 共享变量 :如果一个变量在多个线程的工作内存中 都存在副本 ,那么这个变量就是这几个线程的共享变量。 上面的工作内存其实是java内存模型 抽象出来的概念 ,下面简要介绍一下java内存模型(JMM)。 java内存模型( java memory model ): 描述了java程序中各

    2024年02月10日
    浏览(47)
  • java八股文面试[多线程]——ThreadLocal底层原理和使用场景

    源码分析: ThreadLocal中定义了ThreadLocalMap静态内部类,该内部类中又定义了Entry内部类。 ThreadLocalMap定了 Entry数组。 Set方法: Get方法: Thread中定义了两个ThreaLocalMap成员变量: Spring使用ThreadLocal解决线程安全问题  我们知道在一般情况下,只有 无状态的Bean 才可以在多线程环

    2024年02月10日
    浏览(52)
  • java八股文面试[多线程]——为什么要用线程池、线程池参数

     速记7个: 核心、最大 存活2 队列 工厂 拒绝 线程池处理流程: 线程池底层工作原理: 线程复用原理:   知识来源: 【并发与线程】为什么使用线程池,参数解释_哔哩哔哩_bilibili 【并发与线程】线程池处理流程_哔哩哔哩_bilibili 【并发与线程】线程池的底层工作原理_哔哩

    2024年02月11日
    浏览(51)
  • java八股文面试[多线程]——sleep wait join yield

          sleep和wait有什么区别 sleep 方法和 wait 方法都是用来将线程进入 阻塞状态 的,并且 sleep 和 wait 方法都可以响应 interrupt 中断,也就是线程在休眠的过程中,如果收到中断信号,都可以进行响应并中断,且都可以抛出 InterruptedException 异常,那 sleep 和 wait 有什么区别呢?

    2024年02月11日
    浏览(45)
  • 【面试系列】八股文之线程篇202306

    union all :包含重复行 union :不包含重复行 shutdown() ,调用shutdown方法,线程池会拒绝接收新的任务,处理中的任务和阻塞队列中的任务会继续处理。 shutdownNow() ,会给workers中所有的线程发送 interrupt 信号,将延迟队列的任务移除并返回。 原理分析 执行任务,尝试添加线程。

    2024年02月12日
    浏览(47)
  • 【面试八股文】每日一题:谈谈你对线程的理解

    每日一题-Java核心-谈谈你对线程的理解【面试八股文】   Java线程是Java程序中的执行单元。一个Java程序可以同时运行多个线程,每个线程可以独立执行不同的任务。线程的执行是并发的,即多个线程可以同时执行。   Java中的线程有如下的特点 轻量级:线程的创建和销毁

    2024年02月12日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包