16 synchronized底层如何实现,锁的升级、降级及其他锁

synchronized底层如何实现,锁的升级、降级及其他锁

  • synchronized是java实现同步的关键字,使用monitorenter/monitorexit指令实现;现代JVM提供了三种不同的monitor实现,即:偏斜锁、轻量级锁、重量级锁;在JVM检测到不同的竞争状态时会切换到不同的锁实现,这种优化synchronized运行的过程就叫做锁的升级、降级;
  • 当没有竞争出现时,默认使用偏斜锁,JVM使用CAS操作在对象头的MarkWord部分设置线程ID,表示当前对象偏向某个线程;并不涉及真正的互斥锁;这样做的原因在于,在很多场景中,大部分对象生命周期内最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销;
  • 如果有别的线程试图获取某一个已经偏斜的对象,JVM就需要撤销偏斜锁,并切换到轻量级锁实现。轻量级锁同样使用CAS操作对象头的MarkWord实现,如果成功则获取轻量级锁,否则膨胀为重量级所;
  • 锁也存在降级,JVM在进入安全点时,会检查是否有闲置的monitor并试图降级;

对象头:

普通对象对象头分两部分,每个部分在的32位虚拟机上占用32bit,64位虚拟机上占用64bit;

  1. MarkWord:后面重点介绍;
  2. Class Metadata:指向该对象类信息的指针;

数组类型则多了一个标记的数组大小的部分,这部分在32位虚拟机上是占用32位,在64位虚拟机上的占用空间是否是64bit则需要再查询资料确定;

  1. array length:记录数组长度;

MarkWord:

  • MarkWord用来记录对象的运行时数据,如HashCode、GC分代年龄、锁标志等,需要记录的信息超过了32位、64位所能记录的信息,但考虑到该部分是与对象数据无关的额外空间占用,所以根据对象的状态复用存储空间以提高空间利用率;

      |-------------------------------------------------------|--------------------|
      |                  Mark Word (32 bits)                  |       State        |
      |-------------------------------------------------------|--------------------|
      | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
      |-------------------------------------------------------|--------------------|
      |  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
      |-------------------------------------------------------|--------------------|
      |               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
      |-------------------------------------------------------|--------------------|
      |               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
      |-------------------------------------------------------|--------------------|
      |                                              | lock:2 |    Marked for GC   |
      |-------------------------------------------------------|--------------------|
    
    
    
      |------------------------------------------------------------------------------|--------------------|
      |                                  Mark Word (64 bits)                         |       State        |
      |------------------------------------------------------------------------------|--------------------|
      | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
      |------------------------------------------------------------------------------|--------------------|
      | thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
      |------------------------------------------------------------------------------|--------------------|
      |                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
      |------------------------------------------------------------------------------|--------------------|
      |                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
      |------------------------------------------------------------------------------|--------------------|
      |                                                                     | lock:2 |    Marked for GC   |
      |------------------------------------------------------------------------------|--------------------|
    

biased_lock 1bit、lock的2bit位含义:

上表中的不同的state则是使用biased_lock 1bit和lock的2bit来进行标识的:

biased_lock:0 lock:01; 正常状态;

biased_lock:1 lock:01; 可偏向状态;

lock:00;轻量级锁;

lock:10;重量级锁;

lock:11;GC标记;

开启偏斜锁

-XX:+UseBiasedLocking:开启偏向锁
-XX:BiasedLockingStartupDelay=0,立即开启,因为默认偏向锁的开启时在虚拟机运行后延时5秒

锁的升级、降级:

偏向锁-无锁:

  • 这里比较有疑问的是正常状态和可偏向状态,有一种观点认为:对象一开始可能出于可偏向状态并非正常状态,此时线程指针为0,称为匿名偏向状态,调用Object的hashCode函数或者System.identityHashCode(Object o)函数,他们是等价的,则会计算identity_hashcode并将其存放至对应的MarkWord部分,对象变为正常状态,一旦存放了identity_hashcode,那么该对象将不再允许偏斜,biased_lock会被设置为0,之后该对象将不会在进入偏向状态;
  • 如果在计算hashcode时,该对象不是匿名偏向状态,而是偏向状态,则会直接膨胀为重量级锁;
  • 如果没有线程竞争,非匿名偏向锁偏向锁释放后会变回匿名偏向锁状态;
  • 正常状态下的对象被获取锁时,会直接进入轻量级锁;

锁升级-轻量级锁-重量级锁;

  • 当别的线程试图获取一个已经偏斜的对象时,将会膨胀为轻量级锁,由持有锁的线程在安全点进行,轻量级锁会创建一个LockRecord在当前持有者线程的线程栈中,LockRecord包含一个owner,指向锁对象,锁对象MarkWord也会保存该LockRecord的指针;LockRecord还将保持对象之前的MarkWord内容;
  • 升级为轻量级锁后,竞争线程将在一段时间内自旋,尝试获取锁,如果失败,则将MarkWord中lock标志设置为10并进入阻塞状态;
  • 持有锁的线程在释放锁时会检查MarkWord是否变化,无变化则释放轻量级锁,将MarkWord替换回去;如果MarkWord变化了,则证明已经升级为重量级锁,线程需要将锁升级为重量级锁,并唤醒阻塞的线程;
    锁获取流程图:


    锁获取流程.png

concurrent包下的其他锁

  • Lock:接口,典型的实现类为ReentrantLock
  • ReadWriteLock:接口,典型实现ReentrantReadWriteLock,其逻辑在于并发读之间是不需要互斥的,在应用的并发读多并发写少时,可以考虑使用,因为其本身的复杂性,开销要比lock高;
  • StampedLock:类,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着读,然后通过 validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁;

StampedLock使用示例:

public class StampedSample {
  private final StampedLock sl = new StampedLock();

  void mutate() {
      long stamp = sl.writeLock();
      try {
          write();
      } finally {
          sl.unlockWrite(stamp);
      }
  }

  Data access() {
      long stamp = sl.tryOptimisticRead();
      Data data = read();
      if (!sl.validate(stamp)) {
          stamp = sl.readLock();
          try {
              data = read();
          } finally {
              sl.unlockRead(stamp);
          }
      }
      return data;
  }
  // …
}

你可能感兴趣的:(16 synchronized底层如何实现,锁的升级、降级及其他锁)