锁的升级、降级

  • 所谓所的升级、降级,就是JVM 优化synchronized的运行机制,当jvm检测到不同的竟态条件,会自动切换到合适的锁实现,这种切换情况就是锁的升级、降级
  • synchronized代码块有一对monitorenter/monitorenter指令实现,monitor对象是同步实现的基本单元
  • java6之前,monitor实现完全依靠操作系统内部的互斥锁,因为需要完成用户态到内核态的切换,所以同步操作是一个无差别的重量级操作
  • 现代(oracle)JDK 中,JVM 对此进行重大改进,提供欧冠你三种不同monitor实现,也就是常说的三种不同锁,偏斜锁(Biased Locking),轻量级锁和重量级所,根据不同竟态条件去选择不同锁

jvm 锁切换策略

  • 当无竟态条件出现时,默认是偏斜锁。jvm会使用CAS (compare and swap)操作,在对象头上的Mark Word位置 设置线程ID ,以表示这个对象偏向于当前线程,并不涉及真正的互斥锁。这样做的假设适用多数应用场景(竟态条件),因为大部分对象生命周期内最多会被一个线程锁定。
  • 如果有另外线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS 操作Mark Word来试图获取锁。若重试成功,就使用轻量级级锁;否则进一步升级为重量级锁。
  • JVM在进入安全点(safepoint)时,会检查是否有闲置Monitor ,然后试图进行降级。

知识扩展

JVM synchronized源码分析

  • synchronized是JVM 内部的Instrinsic Lock。所以偏斜锁、轻量级锁、重量级锁的代码实现不在核心类库部分,而是JVM 代码中。
  • java代码运行分两种模式,解释型: javac经编译成字节码后,运行时字节码被jvm加载、解释为机器码进行解释执行。编译型:javac编译成字节码后,jvm的JIT(java 即时编译器)会根据jvm代码情况,执行效率使用内联、虚化等规则,在运行时会将热点代码编译成机器码进行优化,提高执行效率
1.[解释器版本-最新代码](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/interpreter/interpreterRuntime.cpp)
2.为便于理解,这里专注于[通用基类-最新代码](http://hg.openjdk.java.net/jdk/jdk/file/6659a8f57d78/src/hotspot/share/runtime/)实现

偏斜锁代码分析

  • 1.首先,synchronized行为是JVM runtime的一部分,所以我们需要先找到Runtime相关功能实现,通过在代码搜索“monitor_enter”或”monitor Enter”,很直观的就可以定位到:
a.解释器和编译器运行时的基类:sharedRuntime.cpp/hpp,UseBiasedLocking是一个检查,因为,在JVM 启动时,我们可以指定是否开启偏斜锁。偏斜锁不适合所有场景,撤销(revoke)是比较重的行为,只有当存在较多真正竞争的synchronized块时,才能体现出明显改善。

	Handle h_obj(THREAD, obj);
		if (UseBiasedLocking) {
		 // Retry fast entry if bias is revoked to avoid unnecessary inflation
					ObjectSynchronizer::fast_enter(h_obj, lock, true, CHECK);
		  } else {
				 ObjectSynchronizer::slow_enter(h_obj, lock, CHECK);
			 }
b.另一方面,偏斜锁会延缓JIT 预热进程,因此大多数性能测试会显式地关闭偏斜锁
			命令:开启 -XX:+UseBiasedLocking 关闭:-XX:+UseBiasedLocking
  • 2.faster_enter是我们熟悉的完整锁的获取路径,slow_enter则是绕过偏斜锁,直接进入轻量级所获取逻辑,核心实现逻辑如下
(1)类似faster_enter实现,解释器或动态编译器,都是拷贝这的基础逻辑,所以如果我们修改这个部分逻辑,要保证一致性。修改要谨慎,微小问题都可能造成死锁或正确性问题。
(2)逻辑如下:bisedLocking定义了偏斜锁的相关操作,revoke_and_rebias是获取偏斜锁的入口方法,revoke_at_safepoint则定义了当检测到安全点是的处理逻辑;如果获取偏斜锁失败,则进入slow_enter;该方法同样检查是否开启偏斜锁,但从代码看,如果关闭偏斜锁,不会进入该方法,所以其是个额外保障性检查
(3)核心代码如下:
	void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock,
	                                  bool attempt_rebias, TRAPS) {
	  if (UseBiasedLocking) {
	    if (!SafepointSynchronize::is_at_safepoint()) {
	      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
	      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
	        return;
	      }
	  } else {
	      assert(!attempt_rebias, "can not rebias toward VM thread");
	      BiasedLocking::revoke_at_safepoint(obj);
	  }
	    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
	  }
	 
	  slow_enter(obj, lock, THREAD);
	}
仔细查看 synchronized.cpp里,会发现不仅仅是synchronized的逻辑,包括从本地代码(JNI),出发Monitor 动作,都可以看到
  • 4.对象头的Mark Word
  • 5.随着线程竞争加剧,偏斜锁升级为轻量级锁,即slow_enter
流程:设置 Displaced Header,然后利用cas_set_mark设置对象Mark Word,如果成功就获取轻量级锁;否则Displaced Header,然后进入锁膨胀阶段,具体实现在inflated方法中,
	void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
	  markOop mark = obj->mark();
	 if (mark->is_neutral()) {
	       // 将目前的Mark Word复制到Displaced Header上
	  lock->set_displaced_header(mark);
	  // 利用CAS设置对象的Mark Word
	    if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
	      TEVENT(slow_enter: release stacklock);
	      return;
	    }
	    // 检查存在竞争
	  } else if (mark->has_locker() &&
	             THREAD->is_lock_owned((address)mark->locker())) {
	  // 清除
	    lock->set_displaced_header(NULL);
	    return;
	  }
	 
	  // 重置Displaced Header
	  lock->set_displaced_header(markOopDesc::unused_mark());
	  ObjectSynchronizer::inflate(THREAD,
	                            obj(),
	                              inflate_cause_monitor_enter)->enter(THREAD);
	}
- 6。deflate_idle_monitors(参考源码是分析锁降级逻辑的入口,这部分行为会进行持续改进,因为其逻辑实在安全点内进行,处理不当会托长JVM停顿时间(STW,stop-the-world)的时间
-  7. fast_exit或slow_exit是对应的锁释放逻辑(参考源码)

Lock设计

  • 类图
  • 上述显示,这些锁都未实现Lock接口。ReadWriteLock是一个单独接口,通常代表一对锁(共享读锁,互斥写锁)。标准库提供了再入版本的读写锁(ReentrantReadWriteLock),对应的语义同ReentrantLock相似。
  • StampedLock也是单独类型,其不支持再入性予以,即不是以持有锁的线程为单位。其在提供类似读写锁的同时,还支持基于假设的优化读模式。逻辑是先试着读,然后通过validate方法确认是否进入写模式,如果未进入,则成功避免开销;如果进入,则尝试获取读锁。例子如下
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;
  }
  // …
}
  • 读写锁的需求场景
1.ReentrantLock和synchronized简单实用,但行为有一定局限性,通俗说就是”霸道”,要么不占用,要么独占(写时独占保证数据一致性,读时独占则严重影响线程并发).实际场景,不需要大量竞争的写操作,而是以并发读取为主。因此出现ReentrantReadWriteLock等
2.java并发包提供的读写锁扩展了锁的能力,其原理是多个读操作不需要互斥,读操作不会更改数据,不存在相互干扰,写操作则会导致并发一致性问题。因此,写线程之间、读写线程之间,需要精心设计互斥逻辑
3.以下是使用ReentrantReadWriteLock,解决数据量打,并发读多,并发写少,能够比纯同步版本凸显优势。运行过程中,读锁试图锁定时,写锁是被某个线程持有,读锁则无法获取,只能等待对方操作结束,这样保证不会读取到有争议的数据。读锁可以并发访问。
4.这里writeLock和unLockWrite一定保证成对调用
5.例子如下:
public class RWSample {
  private final Map<String, String> m = new TreeMap<>();
  private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  private final Lock r = rwl.readLock();
  private final Lock w = rwl.writeLock();
  public String get(String key) {
      r.lock();
      System.out.println("读锁锁定!");
      try {
          return m.get(key);
      } finally {
          r.unlock();
      }
  }

  public String put(String key, String entry) {
      w.lock();
  System.out.println("写锁锁定!");
        try {
            return m.put(key, entry);
        } finally {
            w.unlock();
        }
    }
  // …
  }