编程语言
首页 > 编程语言> > Java中的偏向锁,轻量级锁, 重量级锁解析

Java中的偏向锁,轻量级锁, 重量级锁解析

作者:互联网

参考文章

Java 中的锁

在 Java 中主要2种加锁机制:

这两种机制的底层原理存在一定的差别

一些先修知识

先修知识 1: Java 对象头

下面的图片来自参考论文Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing, 可以与上面的表格进行比对参照, 更为清晰, 可以看出来, 标志位(tag bits)可以直接确定唯一的一种锁状态

在这里插入图片描述

先修知识 2: CAS 指令

function cas(p , old , new ) returns bool {
    if *p ≠ old { // *p 表示指针p所指向的内存地址
        return false
    }
    *p ← new
    return true
}

先修知识 3: “CAS”实现的"无锁"算法常见误区

// 下列的函数如果不是线程互斥的, 是错误的 CAS 实现
function cas( p , old , new) returns bool {
    if *p ≠ old { // 此处的比较操作进行时, 可以同时有多个线程通过该判断
        return false
    }
    *p ← new // 多个线程的赋值操作会相互覆盖, 造成程序逻辑的错误
    return true
}

先修知识 4: 栈帧(Stack Frame) 的概念

先修知识 5: 轻量级加锁的过程

图2.1

在这里插入图片描述

先修知识 6: 重量级加锁的过程

synchronized 关键字之锁的升级(偏向锁->轻量级锁->重量级锁)

前面提到过, synchronized 代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制

上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。

注意 JVM 提供了关闭偏向锁的机制, JVM 启动命令指定如下参数即可

-XX:-UseBiasedLocking

下图展现了一个对象在创建(allocate) 后, 根据偏向锁机制是否打开, 对象 MarkWord 状态以不同方式转换的过程

这里写图片描述

上图在参考文章一中的中文翻译对照图如下

在这里插入图片描述

无锁 -> 偏向锁

在这里插入图片描述

从上图可以看到 , 偏向锁的获取方式是将对象头的 MarkWord 部分中, 标记上线程ID, 以表示哪一个线程获得了偏向锁。 具体的赋值逻辑如下:

这里写图片描述

下面是 Open Jdk/ JDK 8 源码 中检测一个对象是否处于可偏向状态的源码

  // Indicates that the mark has the bias bit set but that it has not
  // yet been biased toward a particular thread
  bool is_biased_anonymously() const {
    return (has_bias_pattern() && (biased_locker() == NULL));
  }


// Biased Locking accessors.
  // These must be checked by all code which calls into the
  // ObjectSynchronizer and other code. The biasing is not understood
  // by the lower-level CAS-based locking code, although the runtime
  // fixes up biased locks to be compatible with it when a bias is
  // revoked.
  bool has_bias_pattern() const {
    return (mask_bits(value(), biased_lock_mask_in_place) == biased_lock_pattern);
  }
  JavaThread* biased_locker() const {
    assert(has_bias_pattern(), "should not call this otherwise");
    return (JavaThread*) ((intptr_t) (mask_bits(value(), ~(biased_lock_mask_in_place | age_mask_in_place | epoch_mask_in_place))));
  }

从上面的偏向锁机制描述中,可以注意到

偏向锁的撤销(Revoke)

如上文提到的, 偏向锁的撤销(Revoke) 操作并不是将对象恢复到无锁可偏向的状态, 而是在偏向锁的获取过程中, 发现了竞争时, 直接将一个被偏向的对象“升级到” 被加了轻量级锁的状态。 这个操作的具体完成方式如下:

偏向锁的批量再偏向(Bulk Rebias)机制

偏向锁这个机制很特殊, 别的锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。

那么作为开发人员, 很自然会产生的一个问题就是, 如果一个对象先偏向于某个线程, 执行完同步代码后, 另一个线程就不能直接重新获得偏向锁吗? 答案是可以, JVM 提供了批量再偏向机制(Bulk Rebias)机制

该机制的主要工作原理如下:

简而言之,在安全点检查持有C实例的线程栈,如果有的话,将C实例中的epoch值更新,代表还有线程锁定中。

上述的逻辑可以在 JDK 源码中得到验证。

sharedRuntime.cpp

在 sharedRuntime.cpp 中, 下面代码是 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);
  }
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);
}

revoke_and_rebias 函数的定义在 biasedLocking.cpp

BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) {
  assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint");

  // We can revoke the biases of anonymously-biased objects
  // efficiently enough that we should not cause these revocations to
  // update the heuristics because doing so may cause unwanted bulk
  // revocations (which are expensive) to occur.
  markOop mark = obj->mark();
  if (mark->is_biased_anonymously() && !attempt_rebias) {
      /* 
		    进一步查看源码可得知, is_biased_anonymously() 为 true 的条件是对象处于可偏向状态, 
		    且 线程ID  为空, 表示尚未偏向于任意一个线程。 
		    此分支是进行对象的 hashCode 计算时会进入的, 根据 markWord 结构可以看到, 
		    当一个对象处于可偏向状态时, markWord 中 hashCode 的存储空间是被占用的
		    所以需要 revoke 可偏向状态, 以提供存储 hashCode 的空间
		 */
    
    // We are probably trying to revoke the bias of this object due to
    // an identity hash code computation. Try to revoke the bias
    // without a safepoint. This is possible if we can successfully
    // compare-and-exchange an unbiased header into the mark word of
    // the object, meaning that no other thread has raced to acquire
    // the bias of the object.
    markOop biased_value       = mark;
    markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
    markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
    if (res_mark == biased_value) {
      return BIAS_REVOKED;
    }
  } else if (mark->has_bias_pattern()) {
    Klass* k = obj->klass();
    markOop prototype_header = k->prototype_header();
    if (!prototype_header->has_bias_pattern()) {
      // This object has a stale bias from before the bulk revocation
      // for this data type occurred. It's pointless to update the
      // heuristics at this point so simply update the header with a
      // CAS. If we fail this race, the object's bias has been revoked
      // by another thread so we simply return and let the caller deal
      // with it.
      markOop biased_value       = mark;
      markOop res_mark = obj->cas_set_mark(prototype_header, mark);
      assert(!obj->mark()->has_bias_pattern(), "even if we raced, should still be revoked");
      return BIAS_REVOKED;
    } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { 
      // The epoch of this biasing has expired indicating that the
      // object is effectively unbiased. Depending on whether we need
      // to rebias or revoke the bias of this object we can do it
      // efficiently enough with a CAS that we shouldn't update the
      // heuristics. This is normally done in the assembly code but we
      // can reach this point due to various points in the runtime
      // needing to revoke biases.
      if (attempt_rebias) {
	    /*
			下面的代码就是尝试通过 CAS 操作, 将本线程的 ThreadID 尝试写入对象头中
		*/
        assert(THREAD->is_Java_thread(), "");
        markOop biased_value       = mark;
        markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
        markOop res_mark = obj->cas_set_mark(rebiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED_AND_REBIASED;
        }
      } else {
        markOop biased_value       = mark;
        markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
        markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark);
        if (res_mark == biased_value) {
          return BIAS_REVOKED;
        }
      }
    }
  }

偏向锁 -> 轻量级锁

从之前的描述中可以看到, 存在超过一个线程竞争某一个对象时, 会发生偏向锁的撤销操作。 有趣的是, 偏向锁撤销后, 对象可能处于两种状态。

之所以会出现上述两种状态, 是因为偏向锁不存在解锁的操作, 只有撤销操作。 触发撤销操作时:

轻量级加锁过程:

这里写图片描述

下图引用自博文  聊聊并发(二)Java SE1.6中的Synchronized展示了两个线程竞争锁, 最终导致锁膨胀为重量级锁的过程。

**注意: 下图中第一个标绿 MarkWord 的起始状态是HashCode|age|0|01 是偏向锁未被启用时, 分配对象后的状态, 所以在图中并没有偏向锁这一流程的体现, 是直接从无锁状态进入了轻量级锁的状态

这里写图片描述

重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现, 其具体的详细机制此处暂不展开, 日后可能补充。 此处暂时只需要了解该操作会导致进程从用户态与内核态之间的切换, 是一个开销较大的操作。

 

锁的优缺点对比

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

OkidoGreen 发布了302 篇原创文章 · 获赞 1840 · 访问量 1419万+ 他的留言板 关注

标签:Java,对象,mark,CAS,bias,线程,轻量级,重量级,偏向
来源: https://blog.csdn.net/z69183787/article/details/104497801