• 一步一步学多线程synchronized


      当线程执行请求synchronized方法或块时,monitor会设置几个虚拟逻辑数据结构来管理这些多线程。

      

      

      请求的线程会首先被加入到线程排队队列中,线程阻塞,当某个拥有线程锁的线程unlock之后,则排队队列里的线程竞争上岗(synchronized是不公平竞争锁),如果运行的线程调用对象wait()后就释放锁并进入wait线程集合那边,当调用对象的notify()或notifyall()后,wait线程就到排队那边。

    重量级锁

      在JVM规范中描述:每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

      1、  如果monitor的进入数为0,则该线程进入monitor,如果将进入数设置为1,该线程即为monitor的所有者。

      2、  如果线程已经占有monitor,只是重新进入,则进入monitor的进入数加1.

      3、  如果其他线程已经占用了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。

       Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

      Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。

             但是其本质又是依赖于底层的操作系统的互斥锁(Mutex Lock)来实现的。而操作系统实现线程之间的切换这就需要用户转换到和心态,这个成本非常高,状态之间的转换需要相对较长的时间,这就是为什么Synchronized效率低的原因。

             因此,这种依赖于操作系统互斥锁(Mutex Lock)所实现的锁我们称之为“重量级锁”。

             当多线程环境进入synchronized区域的线程没有竞争时,JVM并不会马上创建重量级锁,而是使用偏向锁或者轻量级锁,当存在资源竞争的情况下才会使用重量级锁。

    轻量级锁

      轻量级锁的核心思想:被加锁的代码不会发生并发,如果发生并发,那就膨胀成重量级锁(膨胀即是锁升级)。

           根据轻量级锁的实现,我们知道虽然轻量级锁不支持并发,遇到并发就要膨胀为重量级锁,但是轻量级锁可以支持多个线程以串行的方式访问同一个锁对象。

    偏向锁

      偏向锁的核心思想:假设加锁的代码自始至终只有一个线程调用,如果发现多于一个线程调用,即使没有线程间竞争,也会把锁升级为轻量级锁。

    自旋锁

      当线程阻塞后,如果进入排队队列需要CPU从用户态转为核心态,尤其当遇到频繁的阻塞和唤醒对CPU来说负荷很重。统计发现,很多对象锁的锁定状态持续的时间很短,此时在这么短的时间内进行线程频繁切换资源耗费严重。所以此时引出了自旋锁的概念。

       所谓“自旋”,就是monitor并不把线程阻塞放入排队队列,而是去执行一个无意义的循环,循环结束后看看是否锁已释放并直接进行竞争上岗步骤,如果竞争不到继续自旋循环,循环过程中线程的状态一直处于running状态。明显自旋锁似的synchronized的对象锁方式在线程之间引入了不公平。但是这样可以保证大吞吐率和执行效率。

       不过虽然自旋锁方式省去了阻塞线程的时间和空间(队列的维护等)开销,但是长时间自旋也是很低效的。所以自旋的次数一般控制在一个范围内,如10,50等(在JDK1.6中默认为10次),在超出这个范围后,线程就进入排队队列。

    自适应自旋锁

      就是自旋的次数是通过JVM在运行时收集的统计信息,动态调整自旋锁的自旋次数上界。

    对象头

             介绍了这几种锁,那么程序是通过什么来实现对象锁的呢?首先来看对象头的结构。

        

      在Hotspot虚拟机的对象头上主要包括两部分数据:Mark Word(标记字段),Klass Pointer(类型指针)。其中Klass Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是各种锁的关键。

      Mark Word中的结构大致如此

      

    轻量级锁的获取和释放

    获取锁

      1、  判断当前对象是否处于无锁状态,若是,则JVM首先将当前线程的栈帧中建立一个名为所记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。否则执行步骤3。

      2、  JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作,如果失败则执行步骤3。

      3、  判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

    释放锁

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

    1、  取出在获取轻量级锁保存在Mark Word中的数据;

    2、  用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明锁释放成功,否则执行3.

    3、  如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放所的同时需要唤醒被挂起的线程。

    偏向锁的释放和获取

    获取锁

      1、  检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁表示为01;

      2、  若为可偏向状态,则测试线程ID是否为为当前线程ID,如果是,则执行步骤5,否则执行步骤3。

      3、  如果线程ID不是当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行步骤4。

      4、  通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块。

      5、  执行同步代码块。

    释放锁

      偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程会是不会主动释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点,步骤如下:

      1、  暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态

      2、  撤销偏向锁,恢复到无锁状态或轻量级锁的状态。

    参考资料

    synchronized、锁、多线程同步的原理是咋样的

    深入分析synchronized的实现原理

  • 相关阅读:
    MySQL 5.7 解压版 安装教程(图文详细)[Windows]
    Markdown测试
    Iterator-Java
    设计模式-Iterator
    【转载】图解Java常用数据结构(一)
    SpringBoot项目打包成jar后,启动脚本
    spring boot 中文乱码问题
    Error: Could not find or load main class org.apache.hadoop.mapreduce.v2.app.MRAppMaster
    Caused by: java.lang.NoClassDefFoundError: javax/el/ELManager
    javax.crypto.BadPaddingException: Given final block not properly padded
  • 原文地址:https://www.cnblogs.com/fhhk/p/7396839.html
Copyright © 2020-2023  润新知