84

JVM基础 -- 浅谈synchronized

 5 years ago
source link: http://zhongmingmao.me/2018/12/31/jvm-basic-synchronized/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
public void foo(Object lock) {
    synchronized (lock) {
        lock.hashCode();
    }
}
public void foo(java.lang.Object);
  descriptor: (Ljava/lang/Object;)V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=4, args_size=2
       0: aload_1
       1: dup
       2: astore_2
       3: monitorenter
       4: aload_1
       5: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
       8: pop
       9: aload_2
      10: monitorexit
      11: goto          19
      14: astore_3
      15: aload_2
      16: monitorexit
      17: aload_3
      18: athrow
      19: return
    Exception table:
       from    to  target type
           4    11    14   any
          14    17    14   any
  1. monitorenter指令和monitorexit指令均会消耗操作数栈上的一个 引用类型 元素,作为所要加锁解锁的 锁对象
  2. 上面的字节码包含一个monitorenter指令和多个monitorexit指令
    • JVM需要保证所获得的锁在 正常执行路径 以及 异常执行路径 上都能够被 解锁
    • 具体的执行路径请参照 Exception table

实例方法 + 静态方法

public synchronized void eoo(Object lock) {
    lock.hashCode();
}
public synchronized void eoo(java.lang.Object);
  descriptor: (Ljava/lang/Object;)V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=1, locals=2, args_size=2
       0: aload_1
       1: invokevirtual #2                  // Method java/lang/Object.hashCode:()I
       4: pop
       5: return
  1. 字节码中方法的访问标记(flags)包括 ACC_SYNCHRONIZED
  2. 进入该方法时,JVM需要执行monitorenter操作
  3. 不管正常返回还是向调用者抛出异常,JVM均需要执行monitorexit操作
  4. 这里的monitorenter操作和monitorexit操作 所对应的锁对象是隐式
    • 对于 实例方法 来说,锁对象为 this
    • 对于 静态方法 来说,锁对象为 所在类的Class实例

monitorenter + monitorexit

  1. 抽象理解:每个 锁对象 拥有一个 锁计数器指向持有该锁的线程的指针
  2. 当执行monitorenter时,如果锁对象的计数器为0
    • 那么说明锁对象还没有被其他线程所持有
    • JVM会将锁对象的持有线程设置为当前线程,并将计数器+1
  3. 当执行monitorenter时,如果锁对象的计数器不为0
    • 如果锁对象的持有线程是当前线程,那么JVM会将其计数器+1
    • 否则需要等待,直到持有该锁对象的线程释放该锁
  4. 当执行monitorexit时,JVM则需要将该锁对象的计数器-1
    • 当计数器减为0时,那么代表该锁已经被释放掉了
  5. 采用计数器的方式,是为了允许同一个线程重复获取同一把锁, 可重入

锁优化

对象状态图

rUBz2qJ.gif

针对 一个对象的整个生命周期 ,锁升级是 单向不可逆 :偏向锁 -> 轻量级锁 -> 重量级锁

重量级锁

  1. 重量级锁是JVM中最为基础的锁实现
    • JVM会阻塞加锁失败的线程,并在目标锁被释放掉的时候,唤醒这些线程
    • Java线程的 阻塞唤醒 ,都依赖于 操作系统 完成
    • 涉及系统调用,需要从操作系统的 用户态切换至内核态开销很大
    • 每个 Java对象 都存在一个与之关联的 monitor对象
  2. 为了尽量避免昂贵的线程阻塞和唤醒操作,采用 自旋
    • 自旋:在处理器上 空跑 并且 轮询锁是否被释放
    • 线程会进入自旋的两种情况( 睡前醒后
      • 线程即将 进入阻塞状态之前
      • 线程 被唤醒后竞争不到锁
    • 与线程阻塞相比,自旋可能会 浪费大量的处理器资源
      • JVM采取的是 自适应自旋
      • 根据以往 自旋等待时间 是否能够获得锁来动态调整自旋的时间(循环次数)
    • 自旋还会带来 不公平锁
      • 处于阻塞状态的线程,没有办法立即竞争被释放的锁
      • 而处于自旋状态的线程,则很有可能优先获得这把锁

轻量级锁

  1. 场景: 多个线程在不同的时间段请求同一把锁,没有锁竞争
  2. JVM采用轻量级锁,可以避免 避免重量级锁的阻塞和唤醒

加锁

  1. 如果锁对象标记字段的最后两位为 01无锁或偏向锁
  2. JVM会在 当前栈帧 中建立一个名为 锁记录 (Lock Record)的内存空间
    • 用于存储锁对象当前的标记字段(Mark Word)的拷贝,叫作 Displaced Mark Word
  3. 拷贝锁对象的标记字段到锁记录中,见下图:轻量级锁CAS操作之前
  4. JVM使用 CAS操作
    • 锁对象的标记字段更新为指向锁记录的指针
    • 锁记录里面的owner指针指向锁对象
  5. 如果CAS更新成功,那么当前线程 拥有 了该锁对象的 ,并将锁对象标记字段的锁标志位设置为 00
    • 此时该锁对象处于轻量级锁的状态,见下图:轻量级锁CAS操作之后
  6. 如果CAS更新失败,检查 锁对象的标记字段 是否指向 当前线程的栈帧
    • 如果是,说明 锁重入 ,继续执行同步代码
    • 如果不是,说明出现多线程竞争,膨胀为 重量级锁
      • 锁标记位变为 10 ,锁对象的标记字段存储的是 指向monitor对象的指针
      • 后面等待锁的线程进入阻塞状态,当前线程则尝试使用自旋来获取锁

轻量级锁CAS操作之前

YR3Ezyi.png!web

轻量级锁CAS操作之后

E7FnauY.png!web

解锁

  1. JVM通过 CAS操作把线程中的Displaced Mark Word复制回锁对象的标记字段
  2. 如果CAS更新成功,同步过程结束
  3. 如果CAS更新失败,说明其他线程尝试获取过该锁( 现已膨胀 ),在释放锁的同时, 唤醒被挂起的线程

偏向锁

  1. 无多线程竞争 的情况下,仍然需要 尽量减少不必要的轻量级锁的执行路径
    • 轻量级锁的获取和释放 依赖多次CAS原子指令
    • 由此引入了偏向锁
  2. 偏向锁乐观地认为: 从始至终只有一个线程请求某一把锁
    • 只需要在置换ThreadID时需要依赖一次CAS操作
    • 一旦出现多线程 竞争 ,必须 撤销 偏向锁
    • 轻量级锁是为了在 线程交替执行 同步块时提高性能
    • 偏向锁是为了在 只有一个线程执行 同步块时进一步提高性能

加锁

  1. 偏向锁只会在 第1次 加锁时采用CAS操作
  2. 如果锁对象的标记字段最后三位为 101 ,即 可偏向状态
  3. 锁对象处于 未偏向 状态(Thread ID == 0)
    • 那么JVM会通过 CAS操作 ,将 当前线程的ID 记录在该 锁对象的标记字段
    • 如果CAS成功,则认为当前线程获得该锁对象的偏向锁
    • 如果CAS失败,说明另外一个线程抢先获取了偏向锁
      • 此时需要 撤销偏向锁 ,使得锁对象进入 轻量级锁 状态
  4. 锁对象处于 已偏向 状态(Thread ID != 0)
    • 检测: 标记字段中的线程ID == 当前线程的ID?
    • 如果是,说明 锁重入 ,直接返回
    • 如果不是,说明该锁对象目前偏向于其他线程,需要 撤销偏向锁
    • 一个线程执行完同步代码后, 不会将标记字段的线程ID清空 ,最小化开销

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK