• synchronized面试题总结


    1 偏向锁

      当使用synchronized锁定某一个对象的时候,如果该对象处于匿名偏向状态,就是markword的64位二进制表示偏向线程的bit位都是0,并且锁状态是偏向状态。

      此时该线程直接在这个锁对象的markword高位内存储 当前线程的内存地址,当然了这部分是CAS完成的

      如果CAS成功,这把锁就偏向当前的线程了。

      1 还要向当前的线程栈添加一条锁记录,让锁记录中的锁标识指向当前锁对象

      2 通过CAS设置锁对象的markword,存储当前的线程地址

    2 偏向锁没法做到线程间的互斥,为什么还要这把锁呢

      有时候开发者认为某段代码存在竞争,其实可能实际跑起来并不存在竞争,很可能只有一个线程在执行

      而且偏向锁比轻量级锁损耗更小,因为它在释放锁的时候不需要CAS

    3 偏向锁的释放

      jvm在处理monitorexit这个指令的时候,第一步当前线程栈内与 这个锁对象相关的锁记录全部拿到,并释放掉最后一条锁记录。偏向锁对象仍然保持着偏向状态

    4 这么做有什么好处呢

      为了下次获取锁更快,下次获取锁只需要对比线程地址就行了,不用再次CAS了

    5 轻量级锁

      轻量级锁仍然解决不了线程竞争问题,它并不能提供线程之间的互斥性。轻量级锁对于线程交替运行的情况有很好的性能

    6 轻量级锁的加锁过程

       1 从无锁到轻量级锁

        在当前线程的线程栈插入一条锁记录,锁记录内锁引用字段保存锁对象的地址,让当前的锁对象生成一条无锁状态的markword值,命名是displacedMarkWord

        把这个displacedMarkWord保存到锁记录的displaced字段内,注意是无锁状态的markword。为啥呢?锁释放还得写回去啊,不先保存无锁状态释放的怎么回写呢?

        通过CAS的方式设置当前锁对象的markword值,修改为 当前线程持有轻量级锁状态,如果当前对象是无锁状态就能修改成功

    7 轻量级锁重入

      在收到monitorenter的时候,向当前线程栈内插入一条锁记录,锁记录的obj字段保存该锁对象的内存地址。

      还会再生成一条无锁状态的markword,同时保存到锁记录的displaced字段内。

      还是会通过CAS的方式设置锁对象的markword,所以CAS会失败,然后当前线程会检查失败原因,如果当前锁对象保存的线程地址就是当前线程的话,这就是重入。如果是重入就把刚刚插入的锁记录的displaced字段置为空,流程结束

    8 轻量级锁的释放

      monitorexit,从当前线程的栈中找到最后一条锁的obj字段指向当前锁对象的锁记录,然后把这条lockrecord设置为null,然后检查他的displaced字段是否有值,如果为null,那说明说明这个lockrecord是重入的释放放入的。释放的时候就是把锁记录的obj字段设置为null,这样就完成了退出。

      如果是最后一个recordlock释放的话,释放的过程也是吧obj设置为null,然后检查锁记录,如果第一次那么这个lockrecord的displacedMarkWord保存的是无锁状态的markword,此时就要把这个无锁状态的markword通过CAS方式再放回到对象的markword中。

     9 偏向锁的升级

      当前偏向锁偏向线程A

      线程B首先向线程栈内插入一条锁记录,锁记录的引用字段指向当前锁对象,发现当前锁对象处于偏向锁,并且偏向不是自己

      线程B会提交一个撤销偏向锁的任务,这个任务会提交到VM线程的任务队列中,VM线程在后台不停地处理任务队列内的任务,VM取一个任务之后

      看当前任务是否需要在安全点状态下执行,如果是那就等待安全点。

      简单说下安全点,当处于安全点的时候,JVM所有的线程都处于阻塞状态。只有VM线程处于运行状态,VM可以处理一些特殊任务,比如Full GC,撤销偏向锁,

      撤销偏向锁就必须在安全点内执行,因为撤销的过程中会修改持有锁的线程的桢,所以必须在安全点内执行

      撤销偏向锁首先要检查JVM中当前所有的存活线程,首先检查持有偏向锁的线程是否存活,

      如果持有偏向锁的线程已经消亡,直接把锁对象markword修改为无锁状态

      如果偏向线程还活着,首先要确定偏向线程是否还在执行同步代码块,这个通过锁记录就能知道了。

      如果已经不在同步代码块了,那就直接把锁对象的markword设置为无锁状态

      如果线程还在同步代码块,此时就是锁升级的过程,首先遍历线程栈,找到锁记录指向锁对象的第一条记录,修改这条记录的displaced字段的值为无锁状态的markword,然后修改锁对象的markword为轻量级锁状态,保留这条锁记录的内存地址,其实就是持有锁的线程的内存空间的第一个位置,轻量级锁的markword保存着

    线程的地址。

    10 什么是重量级锁

      重量级锁和AQS很像,都是遵循管程模型。其在JVM中的类型是ObjectMonitor,它数据结构的核心是三个队列

      CQX队列和EntryList队列,还有waitList(相当于是AQS的条件队列)

      还有一个很重要的当前锁的持有线程owner字段。

      cqx和EntryList是保存竞争线程排队的,waitList用来处理调用了wait()方法后从竞争锁队列转移到等待队列

    11 重量级获取锁过程

      重量级锁markword保存的是管程对象的内存地址和重量级锁的状态值10,通过重量级锁markword保存的管程对象地址,找到管程对象,线程去管程内取抢占锁

      进入管程之后自旋几次尝试获取锁,其实就是通过CAS设置owner字段指向自己,如果CAS成功锁抢占成功

      如果失败把当前线程封装成waiter节点插入到竞争队列,插入成功后把自己挂起,一直等到其他线程唤醒

    12 重量级锁的释放

      首先把ObjectMonitor的 owner = null, 检查竞争队列和EntryList是否有等待的线程,如果有则根据唤醒策略去唤醒一个线程。

      QMode == 1 : 将cxq中的元素转移到EntryList,并反转顺序

           QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;

      QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;

      QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;

      QMode = 0:暂时什么都不做,继续往下看;

    13 锁膨胀的过程

      锁膨胀的三种情况

      1 当调用锁对象的hashcode方法的时候,因为偏向锁和轻量级锁都没法存储hash值,只有重量级锁可以

      2 当调用wait方法的时候,因为只有重量级锁才有管程,才能把这个线程包装成Wait放到WaitSet中

      3 轻量级锁经过竞争膨胀

           这里只讨论轻量级锁竞争导致的膨胀

      假设现在有A正在持有轻量级锁,此时B去竞争,此时B会执行锁膨胀逻辑。锁膨胀逻辑里面线程B会获取一个空闲的管程对象,通过CAS修改锁对象为膨胀中状态,如果CAS失败说明正在膨胀或者膨胀已经结束,那就再次自旋。如果CAS成功了,说明当前线程设置锁状态成功,那么就由它来进行锁升级,将管程对象的owner字段设置为原轻量级锁持有线程,再将持有线程的第一条锁记录内存储的displacedMarkWord保存到管程对象里。再下一步,设置锁对象的markword为重量级锁,包含两个信息,一个是重量级锁也就是ObjectMonitor的内存位置,另一个是改变锁状态。

  • 相关阅读:
    学习OSGI---建项目,运行配置
    MongoDB 安装
    利用 ^ 异或运算符 进行交换(不需要第三方变量)
    2019HDU暑期多校训练-1004equation-方程求解
    HDU 4417-Super Mario-线段树+离线
    HDU 3333-Turing Tree-线段树+离散+离线
    POJ 2528-Mayor's posters-线段树+离散化
    POJ 2631-Roads in the North-树的直径
    POJ 2299-Ultra-QuickSort-线段树的两种建树方式
    noip2009最优贸易——spfa
  • 原文地址:https://www.cnblogs.com/juniorMa/p/14093286.html
Copyright © 2020-2023  润新知