zoukankan      html  css  js  c++  java
  • Java多线程(五)——synchronized关键字原理

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/

    学习自:

    1、Java对象的内存表示

    • Java 对象在内存中的表示方法:

    image-20210209170021782

    • 内存中的对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充。

    • 对象头包含 Mark Word、Class Pointer和 Length 三部分。

      • 标记字段Mark Word 记录了对象关于锁的信息,垃圾回收信息等。
      • Class Pointer 用于指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。
      • Length只适用于对象是数组时,它保存了该数组的长度信息。
    • 对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。

    • 对齐填充表示最后一部分的填充字节位,这部分不包含有用信息。

    • synchronized 锁使用的就是对象头的 Mark Word 字段中的一部分。

      • Mark Word 中的某些字段发生变化,就可以代表锁不同的状态。
      • 由于锁的信息是记录在对象里的,也往往会说锁住对象这种表述。
    • 无锁状态的 Mark Word 字段:

      image-20210209170444269

      • 对象头的 Mark Word 字段分为四个部分:
        1. 对象的 hashCode ;
        2. 对象的分代年龄,这部分用于对对象的垃圾回收;
        3. 是否为偏向锁位,1代表是,0代表不是;
        4. 锁标志位,这里是 01。

    2、synchronized原理

    • 直接作为关键字修饰在方法上,将整个方法作为同步代码块:
      • 编译器会为该方法自动生成了一个 ACC_SYNCHRONIZED 关键字用来标识。
      • 在 JVM 进行方法调用时,当发现调用的方法被 ACC_SYNCHRONIZED 修饰,则会先尝试获得锁。
      • 同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用monitorenter和monitorexit。
    • 修饰在同步代码块上:
      • 编译时在代码块开始前生成对应的1个 monitorenter 指令,代表同步块进入。2个 monitorexit 指令,代表同步块退出。
    • 这两种方法底层都需要一个 reference 类型的参数,指明要锁定和解锁的对象。
      • 如果 synchronized 明确指定了对象参数,那就是该对象。
      • 如果没有明确指定,那就根据修饰的方法是实例方法还是类方法,取对应的对象实例或类对象(Java 中类也是一种特殊的对象)作为锁对象。
      • 每个对象维护着一个记录着被锁次数的计数器。当一个线程执行 monitorenter,该计数器自增从 0 变为 1;
      • 当一个线程执行 monitorexit,计数器再自减。当计数器为 0 的时候,说明对象的锁已经释放。
    • 为什么会有两个 monitorexit 指令呢?
      • 正常退出,得用一个 monitorexit 吧,如果中间出现异常,锁会一直无法释放。所以编译器会为同步代码块添加了一个隐式的 try-finally 异常处理,在 finally 中会调用 monitorexit 命令最终释放锁。

    重量级锁

    • 重量级锁对应对象的 Mark Word:

      image-20210209171048556

      • 该对象头的 Mark Word 分为两个部分。第一部分是指向重量级锁的指针,第二部分是锁标记位。

      • 指向重量级锁的指针就是所谓的同步监视器 monitor。这个监视器其实也就是监控锁有没有释放,释放的话会通知下一个等待锁的线程去获取。

        image-20210209171227328

      • 可以将 monitor 简单理解成两部分,第一部分表示当前占用锁的线程,第二部分是等待这把锁的线程队列。如果当前占用锁的线程把锁释放了,那就需要在线程队列中唤醒下一个等待锁的线程。

      • 是阻塞或唤醒一个线程需要依赖底层的操作系统来实现,Java 的线程是映射到操作系统的原生线程之上的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个状态转换需要花费很多的处理器时间,甚至可能比用户代码执行的时间还要长。由于这种效率太低,Java 后期做了改进。

    • CAS算法:

      1. 该算法认为线程之间对变量的操作进行竞争的情况比较少。
      2. 算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
      3. 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N
      4. 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
      • 当线程运行 CAS 算法时,该运行过程是原子操作,原子操作的含义就是线程开始跑这个函数后,运行过程中不会被别的程序打断。

    偏向锁

    • JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。如果是这种情况,不断的加锁解锁是没有必要的。

    • 因此开发者设计了偏向锁。偏向锁在获取资源的时候,会在资源对象上记录该对象是否偏向该线程。

    • 偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。

    • 偏向锁的 Mark Word结构:

      image-20210209171909655

      • 偏向锁对应的 Mark Word 包含该偏向锁对应的线程 ID、偏向锁的时间戳和对象分代年龄。
    • 偏向锁的申请流程:

      1. 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
      2. 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
      3. 判断是否需要重偏向,如果不用的话,直接获得偏向锁;
      4. 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。

      • 在执行完同步代码后,线程不会主动去修改对象的 Mark Word,让它重回无锁状态。所以一般执行完 synchronized 语句后,如果是偏向锁的状态的话,线程对锁的释放操作可能是什么都不做。
    • 匿名偏向锁:

      • 在 JVM 开启偏向锁模式下,如果一个对象被新建,在四秒后,该对象的对象头就会被置为偏向锁。
      • 一般来说,当一个线程获取了一把偏向锁时,会在对象头和栈帧中的锁记录里不仅说明目前是偏向锁状态,也会存储锁偏向的线程 ID。
      • 在 JVM 四秒自动创建偏向锁的情况下,线程 ID 为0。
      • 由于这种情况下的偏向锁不是由某个线程求得生成的,这种情况下的偏向锁也称为匿名偏向锁。
    • 批量重偏向和批量撤销:

      • 在生产者消费者模式下,生产者线程负责对象的创建,消费者线程负责对生产出来的对象进行使用。
      • 当生产者线程创建了大量对象并执行加偏向锁的同步操作,消费者对对象使用之后,会产生大量偏向锁执行和偏向锁撤销的问题。
      • 以类为单位,为每个类维护一个偏向锁撤销计数器,每一次该类的对象发生偏向撤销操作时,该计数器计数 +1,当这个计数值达到重偏向阈值时,JVM 就认为该类可能不适合正常逻辑,适合批量重偏向逻辑。

    轻量级锁

    • 轻量级锁的设计初衷对于绝大部分的锁,在整个同步周期内都是不存在竞争的。所以它的设计出发点也在线程竞争情况较少的情况下。

    • 轻量级锁的 Mark Word:

      image-20210209174123641

      • 第一部分是指向栈中的锁记录的指针,第二部分是锁标记位,针对轻量级锁该标记位为 00。
    • 偏向锁升级为轻量级锁:

      • 如果当前这个对象的锁标志位为 01(即无锁状态或者偏向锁状态),线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,包括一个用于复制对象头中的 Mark Word 以及一个指向对象的指针。
      • 然后 JVM 会利用 CAS 算法对这个对象的 Mark Word 进行修改。如果修改成功,那该线程就拥有了这个对象的锁。
      • 如果 CAS 失败,那就说明同时执行 CAS 操作的线程可不止一个了, Mark Word 也做了更改。
        • 首先虚拟机会检查对象的 Mark Word 字段指向栈中的锁记录的指针是否指向当前线程的栈帧。如果是,那就说明可能出现了类似 synchronized 中套 synchronized 情况。这种情况下当前线程已经拥有这个对象的锁,可以直接进入同步代码块执行。
        • 否则说明锁被其他线程抢占了,该锁还需要升级为重量级锁。
      • 但是不会直接升级为重量级锁,会先自旋的尝试获取轻量级锁,默认自旋10次后,在升级为重量级锁。

    • 和偏向锁不同的是,执行完同步代码块后,需要执行轻量级锁的解锁过程:

      1. 通过 CAS 操作尝试把线程栈帧中复制的 Mark Word 对象替换当前对象的 Mark Word。
      2. 如果 CAS 算法成功,整个同步过程就完成了。
      3. 如果 CAS 算法失败,则说明存在竞争,锁升级为重量级锁。

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/
    来源与结束于否定之否定。
  • 相关阅读:
    NBUT 1120 Reimu's Teleport (线段树)
    NBUT 1119 Patchouli's Books (STL应用)
    NBUT 1118 Marisa's Affair (排序统计,水)
    NBUT 1117 Kotiya's Incantation(字符输入处理)
    NBUT 1115 Cirno's Trick (水)
    NBUT 1114 Alice's Puppets(排序统计,水)
    188 Best Time to Buy and Sell Stock IV 买卖股票的最佳时机 IV
    187 Repeated DNA Sequences 重复的DNA序列
    179 Largest Number 把数组排成最大的数
    174 Dungeon Game 地下城游戏
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14395062.html
Copyright © 2011-2022 走看看