• 【锁】synchronized的实现(偏向锁、轻量级锁、重量级锁)


    synchronized的三种应用方式

    • 一. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
    • 二. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
    • 三. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象。

    synchronized的字节码指令

    synchronized同步块使用了monitorentermonitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

    线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

    synchronized的锁的原理

    两个重要的概念:一个是对象头,另一个是monitor

    Java对象头

    在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头(Mark Word、Class Metadata Address)、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

    Mark Word
    Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的
    锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,
    也就是32bit)。

    32位JVM的Mark Word的默认存储结构如下:

     25 bit4bit1bit
    是否是偏向锁
    2bit
    锁标志位
    无锁状态 对象的hashCode 对象分代年龄 0 01

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

    Array length
    如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。

    Monitor

    Monitor是一个同步工具,它内置于每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。

    在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。

     

    synchronized锁的优化

    jdk1.6以后对synchronized的锁进行了优化,引入了偏向锁、轻量级锁,锁的级别从低到高逐步升级: 

    无锁->偏向锁->轻量级锁->重量级锁

    自旋锁与自适应自旋

    线程的挂起和恢复会极大的影响开销。并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。这样就避免了线程切换的开销,极大的提升了性能。

    而适应性自旋,是赋予了自旋一种学习能力,它并不固定自旋10次一下。他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

    锁消除

    对不会存在线程安全的锁进行消除。

    锁粗化

    如果jvm检测到有一串零碎的操作都对同一个对象加锁,将会把锁粗化到整个操作外部,如循环体。

    偏向锁

    多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让其获得锁的代价更低而引入了偏向锁。

    • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
    • 如果测试成功,表示线程已经获得了锁。
    • 如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成01(表示当前是偏向锁)。
    • 如果没有设置,则使用CAS竞争锁。
    • 如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

     

    轻量级锁

    引入轻量级锁的主要目的是在多线程竞争不激烈的情况下,通过CAS竞争锁,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

    轻量级锁的执行过程:在代码进入同步块的时候,如果同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前Mark Word的拷贝,这时候线程堆栈与对象头的状态如图所示:

     

    然后虚拟机将使用CAS操作尝试将对象头中的Mark Word 更新为指向当前线程Lock Record的指针,如果这个更新执行成功了,那么这个线程就拥有了这个对象的锁,并且将Mard Word中的标记位改为00,即表示该对象处于轻量级锁状态,这时候线程堆和对象头的状态如图所示:

     

    如果这个状态更新失败了,虚拟机将会检查对象中的Mark Word 是否指向当前线程的栈帧,如果是直接进入同步代码块 执行,如果不是 说明有线程竞争。如果有两条以上的线程在抢占资源,那轻量级锁就不再有效,要膨胀为重量级锁,锁的状态更改为10, Mard Word中存储的就是指向重量级锁的指针 后面等待的锁就要进入阻塞状态。

    重量级锁

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

    锁升级

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

    wait和notify的原理

    调用wait方法,首先会获取监视器锁,获得成功以后,会让当前线程进入等待状态,进入等待队列并且释放锁。

    当其他线程调用notify后,会选择从等待队列中唤醒任意一个线程,而执行完notify方法以后,并不会立马唤醒线程,原因是当前的线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令以后,也就是锁被释放以后,处于等待队列中的线程就可以开始竞争锁了。

    wait和notify为什么需要在synchronized里面?

    wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列,而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。

    而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里,所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。

    锁的优缺点对比

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



    参考自:

      https://blog.csdn.net/u011212394/article/details/82228321

      https://www.cnblogs.com/wade-luffy/p/5969418.html
      https://blog.csdn.net/sinat_41832255/article/details/89309944

  • 相关阅读:
    最近学习的情况
    0Day – 2011.1.12[From B4A]考试终于完了
    PHP 在NetBeans中的测试[转自NetBeans.org]
    12月武汉下雪了
    Delphi下指针使用总结
    0Day – 2010.12.27[转自B4A]
    AQTime教程
    C #define理解
    C 环境的搭配
    数据结构 字符串的模式匹配
  • 原文地址:https://www.cnblogs.com/itplay/p/11189651.html
Copyright © 2020-2023  润新知