• 深入学习重点分析java基础---第二章:java并发 synchronized


    1.由浅入深首先要有 乐观锁、悲观锁的概念

      乐观锁:CAS(比较并替换)  乐观锁从乐观的角度看待并发问题,也就是乐观锁默认不存在并发问题,只是线程去修改数据的时候发现数据已经被修改了,才会返回修改失败的响应,乐观锁允许线程自旋尝试获取锁

      悲观锁:synchronized、reentranLock以及数据库for update等,悲观锁默认会有线程争抢资源.所以每次线程操作前会尝试抢占锁,如果锁已经被占用就阻塞当前线程,并等待持有锁的线程释放锁、

    2.可重入锁

      synchroized和reentranLock都是可重入锁,可重入锁的意思是,当一个线程获取了该锁后,还能再次获取该锁,并且将重入数加1,每次线程释放锁 该计数减1

      为什么要设计为可重入锁?当线程已经持有该对象的monitor锁时再次访问受限资源时monitor计数器会加1,当访问结束jvm释放monitor锁时,monitor计数器会减1,计数器为0则完全释放该对象的monitor锁

    3.synchronized的锁升级

      jdk1.6对synchronized进行了非常大的优化,使得synchronized与reentranLock性能非常接近,甚至在高并发情况下效率高与reentranLock

      整个升级过程为

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

      首先没有线程获取对象的monitor锁时,对象处于无锁状态

      

      当线程获取对象的monitor锁时,会在对象头和栈帧的锁记录里存储当前线程的ID,作为偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁只需测试Mark Word里线程ID是否为当前线程

      如果测试成功,表示线程已经获得了锁。如果测试失败,则需要判断偏向锁的标识。如果标识被设置为0(表示当前是无锁状态),则使用CAS竞争锁;如果标识设置成1(表示当前是偏向锁状态)

      则尝试使用CAS将对象头的偏向锁指向当前线程,触发偏向锁的撤销。偏向锁只有在竞争出现才会释放锁当其他线程尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java

      对象头中记录的线程是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象

      那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程

      需要注意的是对象头中有一个epoch值,该值会记录对象偏向锁改变的次数,可以理解为偏向锁的版本号

      

      这个epoch有什么用呢?就是当虚拟机认为此类对象被撤销的次数超过一定次数 ( -XX:BiasedLockingBulkRebiasThreshold,默认为 20)之后认为该换代了

      也就是每次锁对象被撤销偏向的时候都会记录着,当超过阈值之后,对象对应的类里面的epoch就会升级,也就是+1。

      当类对象被撤销的次数超过一定次数 (-XX:BiasedLockingBulkRevokeThreshold,默认值为 40),JVM会认为此类对象都不适合偏向,会撤销所有此类实例的偏向锁,并且之后的加锁直接轻量级,没偏向了

     

      

      当对象处于轻量级锁状态时,线程在获取monitor时,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,即Displaced Mark Word然后线程会尝试使用CAS将对象头中的Mark Word

      替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁。

      轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回到对象头,如果成功,表示没有竞争发生。如果失败,表示当前锁存在竞争,锁已经被升级为重量级锁,则会释放锁并唤醒等待的线程。

    4.各种锁状态下对象头中的数据

      

      synchronized的锁状态存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位JVM的Mark Word可能变化存储为以下5种数据:

    面试必问---synchronized实现原理及锁升级过程你懂吗?

                        图1

      可以看到当对象处于无锁状态之外的任何状态时,都没有存储HashCode,那么此时如何获取对象的HashCode呢

      首先这里的HashCode指的是jvm中的 identity hash code 与类中自定义的hashCode()方法不是一回事,如果没有自定义hashCode()方法,也就是调用的Object对象的hashCode方法时返回的才是identity hash code

      这里需要注意几点

    • 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
    • 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
    • 重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。

     5.synchroized底层实现原理

      synchroized在未发生激烈竞争的情况下不会进入到重量级锁状态,此前都是使用自旋的方式阻塞线程,而大家都知道自旋锁只在用户态就能做到,只是会消耗cpu资源

      而synchroized进入到重量级锁之后则是切换到内核态阻塞线程,从图1中可以看到重量级锁只存储了指向互斥量(重量级锁)的指针

      这里的指针指向的其实就是上面提到过多次的monitor锁的起始地址

      每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态

      在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(ObjectMonitor.hpp文件,C++实现的)

      

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL;  //_owner指向持有ObjectMonitor对象的线程
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }

      ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象);
      整个monitor运行的机制过程如下:
      _owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1

      若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
      具体见下图:
    在这里插入图片描述
      因此,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因

      同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因

    ps:文中有借鉴以下博客中的内容,感谢几位博主分享了这么多优质内容

    https://blog.csdn.net/mulinsen77/article/details/88635558

    https://www.zhihu.com/question/52116998?sort=created

  • 相关阅读:
    进程锁和进程池(附线程池)
    Python多进程
    queue队列
    随笔:python3+yagmail邮件发送,简单易操作
    随笔:用python的pip命令时却显示没有提供命令:Did not provide a command
    随笔:python+selenium+unittest用qq邮箱上传文件并发送邮件
    随笔:用HtmlTestRunner生成报告
    随笔:Linux里mysql的tar包安装
    Performance Testing 前期准备以及场景设计
    Performance Testing 入门小结
  • 原文地址:https://www.cnblogs.com/buggeerWang/p/13252769.html
Copyright © 2020-2023  润新知