synchronized

Java中的每一个对象都可以作为锁。具体表现 为以下3种形式

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的Class对象
  • 对于同步方法块,锁是Synchonized括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,代码块同步是使用monitorenter 和monitorexit指令实现的

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结 束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对.任何对象都有 一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.线程执行到monitorenter 指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁.

Java对象头

synchronized用的锁是存在Java对象头里的,Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位.

锁的升级与对比

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级.

锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁.这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率

锁转化图

偏向锁

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

偏向锁的获取过程

  1. 访问Mark Word中偏向锁标志位是否设置成1,锁标志位是否为01——确认为可偏向状态
  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码
  5. 执行同步代码

偏向锁的释放

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点safepoint,它会首先暂停拥有偏向锁的线程A,然后判断这个线程A,此时有两种情况

  • 线程不处于活动状态,则将对象头设置成无锁状态
  • 线程仍然活着,将持有线程的偏向锁升级为轻量级锁

关闭偏向锁

通过JVM参数 -XX:- UseBiasedLocking=false,关闭偏向锁,程序默认进入轻量级锁状态

轻量级锁

轻量级锁的获取过程

  1. 在代码进入同步块的时候,如果同步对象锁状态为偏向状态(就是锁标志位为“01”状态,是否为偏向锁标志位为“1”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。官方称之为 Displaced Mark Word
  2. 拷贝对象头中的Mark Word复制到锁记录中
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象头的mark word。如果更新成功,则执行步骤(4),否则执行步骤(5)
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
  5. 如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧.如果是就说明当前线程已经拥有了这个对象的锁,现在是重入状态,那么设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用. 如果不是说明这个锁对象已经被其他线程抢占了,说明此时有多个线程竞争锁,那么它就会自旋等待锁,一定次数后仍未获得锁对象,说明发生了竞争,需要膨胀为重量级锁

轻量级锁的解锁过程

  • 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word
  • 如果替换成功,整个同步过程就完成了
  • 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级 成重量级锁,就不会再恢复到轻量级锁状态

锁的对比

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

锁的其他优化

  • 适应性自旋(Adaptive Spinning) 线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少
  • 锁粗化(Lock Coarsening)将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁
  • 锁消除(Lock Elimination)同步代码块中删除不必要的加锁操作的代码

文章链接 https://fangzongzhou.github.io/2020/10/13/计算机/技术栈/Java/并发编程/synchronized/