farmerjohngit / myblog

有深度的Java技术博客
1.84k stars 287 forks source link

死磕Synchronized底层实现--轻量级锁 #14

Open farmerjohngit opened 6 years ago

farmerjohngit commented 6 years ago

死磕Synchronized底层实现--轻量级锁

本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现。

轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠

另外轻量级锁的背景和基本流程在概论中已有讲解。强烈建议在看过两篇文章的基础下阅读本文

本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁、轻量级锁、重量级锁的加锁、解锁、锁升级流程的原理及源码分析,希望给在研究synchronized路上的同学一些帮助。主要包括以下几篇文章:

死磕Synchronized底层实现--概论

死磕Synchronized底层实现--偏向锁

死磕Synchronized底层实现--轻量级锁

死磕Synchronized底层实现--重量级锁

更多文章见个人博客:https://github.com/farmerjohngit/myblog

本文分为两个部分:

1.轻量级锁获取流程

2.轻量级锁释放流程

本人看的JVM版本是jdk8u,具体版本号以及代码可以在这里看到。

轻量级锁获取流程

下面开始轻量级锁获取流程分析,代码在bytecodeInterpreter.cpp#1816

CASE(_monitorenter): {
  oop lockee = STACK_OBJECT(-1);
  ...
  if (entry != NULL) {
   ...
   // 上面省略的代码中如果CAS操作失败也会调用到InterpreterRuntime::monitorenter

    // traditional lightweight locking
    if (!success) {
      // 构建一个无锁状态的Displaced Mark Word
      markOop displaced = lockee->mark()->set_unlocked();
      // 设置到Lock Record中去
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
        // Is it simple recursive case?
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          // CAS操作失败则调用monitorenter
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    }
    UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
  } else {
    istate->set_msg(more_monitors);
    UPDATE_PC_AND_RETURN(0); // Re-execute
  }
}

如果锁对象不是偏向模式或已经偏向其他线程,则successfalse。这时候会构建一个无锁状态的mark word设置到Lock Record中去,我们称Lock Record中存储对象mark word的字段叫Displaced Mark Word

如果当前锁的状态不是无锁状态,则CAS失败。如果这是一次锁重入,那直接将Lock RecordDisplaced Mark Word设置为null

我们看个demo,在该demo中重复3次获得锁,

synchronized(obj){
    synchronized(obj){
        synchronized(obj){
        }
    }
}

假设锁的状态是轻量级锁,下图反应了mark word和线程栈中Lock Record的状态,可以看到右边线程栈中包含3个指向当前锁对象的Lock Record。其中栈中最高位的Lock Record为第一次获取锁时分配的。其Displaced Mark word的值为锁对象的加锁前的mark word,之后的锁重入会在线程栈中分配一个Displaced Mark wordnullLock Record

为什么JVM选择在线程栈中添加Displaced Mark word为null的Lock Record来表示重入计数呢?首先锁重入次数是一定要记录下来的,因为每次解锁都需要对应一次加锁,解锁次数等于加锁次数时,该锁才真正的被释放,也就是在解锁时需要用到说锁重入次数的。一个简单的方案是将锁重入次数记录在对象头的mark word中,但mark word的大小是有限的,已经存放不下该信息了。另一个方案是只创建一个Lock Record并在其中记录重入次数,Hotspot没有这样做的原因我猜是考虑到效率有影响:每次重入获得锁都需要遍历该线程的栈找到对应的Lock Record,然后修改它的值。

所以最终Hotspot选择每次获得锁都添加一个Lock Record来表示锁的重入。

接下来看看InterpreterRuntime::monitorenter方法

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  ...
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  ...
IRT_END

fast_enter的流程在偏向锁一文已经分析过,如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态,同时会将偏向的线程栈中的Lock Record修改为轻量级锁对应的形式。代码位置在biasedLocking.cpp#212

 // 线程还存活则遍历线程栈中所有的Lock Record
  GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);
  BasicLock* highest_lock = NULL;
  for (int i = 0; i < cached_monitor_info->length(); i++) {
    MonitorInfo* mon_info = cached_monitor_info->at(i);
    // 如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码
    if (mon_info->owner() == obj) {
      ...
      // 需要升级为轻量级锁,直接修改偏向线程栈中的Lock Record。为了处理锁重入的case,在这里将Lock Record的Displaced Mark Word设置为null,第一个Lock Record会在下面的代码中再处理
      markOop mark = markOopDesc::encode((BasicLock*) NULL);
      highest_lock = mon_info->lock();
      highest_lock->set_displaced_header(mark);
    } else {
      ...
    }
  }
  if (highest_lock != NULL) {
    // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    ...
  } else {
    ...
  }

我们看slow_enter的流程。

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 如果是无锁状态
  if (mark->is_neutral()) {
    //设置Displaced Mark Word并替换对象头的mark word
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
  } else
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    // 如果是重入,则设置Displaced Mark Word为null
    lock->set_displaced_header(NULL);
    return;
  }

  ...
  // 走到这一步说明已经是存在多个线程竞争锁了 需要膨胀为重量级锁
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

轻量级锁释放流程

CASE(_monitorexit): {
  oop lockee = STACK_OBJECT(-1);
  CHECK_NULL(lockee);
  // derefing's lockee ought to provoke implicit null check
  // find our monitor slot
  BasicObjectLock* limit = istate->monitor_base();
  BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
  // 从低往高遍历栈的Lock Record
  while (most_recent != limit ) {
    // 如果Lock Record关联的是该锁对象
    if ((most_recent)->obj() == lockee) {
      BasicLock* lock = most_recent->lock();
      markOop header = lock->displaced_header();
      // 释放Lock Record
      most_recent->set_obj(NULL);
      // 如果是偏向模式,仅仅释放Lock Record就好了。否则要走轻量级锁or重量级锁的释放流程
      if (!lockee->mark()->has_bias_pattern()) {
        bool call_vm = UseHeavyMonitors;
        // header!=NULL说明不是重入,则需要将Displaced Mark Word CAS到对象头的Mark Word
        if (header != NULL || call_vm) {
          if (call_vm || Atomic::cmpxchg_ptr(header, lockee->mark_addr(), lock) != lock) {
            // CAS失败或者是重量级锁则会走到这里,先将obj还原,然后调用monitorexit方法
            most_recent->set_obj(lockee);
            CALL_VM(InterpreterRuntime::monitorexit(THREAD, most_recent), handle_exception);
          }
        }
      }
      //执行下一条命令
      UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
    }
    //处理下一条Lock Record
    most_recent++;
  }
  // Need to throw illegal monitor state exception
  CALL_VM(InterpreterRuntime::throw_illegal_monitor_state_exception(THREAD), handle_exception);
  ShouldNotReachHere();
}

轻量级锁释放时需要将Displaced Mark Word替换到对象头的mark word中。如果CAS失败或者是重量级锁则进入到InterpreterRuntime::monitorexit方法中。

//%note monitor_1
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))

  Handle h_obj(thread, elem->obj());
  ...
  ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
  // Free entry. This must be done here, since a pending exception might be installed on
  //释放Lock Record
  elem->set_obj(NULL);
  ...
IRT_END

monitorexit调用完slow_exit方法后,就释放Lock Record

void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
  fast_exit (object, lock, THREAD) ;
}
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  ...
  markOop dhw = lock->displaced_header();
  markOop mark ;
  if (dhw == NULL) {
     // 重入锁,什么也不做
     ...
     return ;
  }

  mark = object->mark() ;

  // 如果是mark word==Displaced Mark Word即轻量级锁,CAS替换对象头的mark word
  if (mark == (markOop) lock) {
     assert (dhw->is_neutral(), "invariant") ;
     if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
        TEVENT (fast_exit: release stacklock) ;
        return;
     }
  }
  //走到这里说明是重量级锁或者解锁时发生了竞争,膨胀后调用重量级锁的exit方法。
  ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}

该方法中先判断是不是轻量级锁,如果是轻量级锁则将替换mark word,否则膨胀为重量级锁并调用exit方法,相关逻辑将在重量级锁的文章中讲解。

DanFL commented 5 years ago

revoke_bias方法代码中有一行注解 // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为执行该Lock Record的指针 此处应说的设置为指向该Lock Record的指针 偏向锁那一章也是如此吧

farmerjohngit commented 5 years ago

@DanFL 已修改 感谢指正

flying81621 commented 5 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,

flying81621 commented 5 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

flying81621 commented 5 years ago

另外,在轻量级锁加锁中,CAS的原值若不是从对象中取,而是通过调用对象的hashcode()方法获取,那如果对象所属类的hashcode方法被重写了,获取锁的线程在代码块里头更改锁对象的某个属性值了,岂不是,当线程释放完锁之后,锁对象头里的hashcode变成了一个过期的脏数据hashcode值,那以后所有的线程都没法对这个对象加锁了?

farmerjohngit commented 5 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

没看懂你在说什么。。你说的是轻量级加锁的cas过程? 原值是无锁的markword,期望值是当前的Lock Record。所以不会有两个线程同时执行同步块

LYoGA commented 5 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功

scn7th commented 5 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功

我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败

flying81621 commented 5 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答

在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道:

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功

我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

zncepup commented 5 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答 在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道: 我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? 大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

我也有这个疑问。

sunjcc commented 5 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答 在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道: 我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? 大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

我理解线程B中存放的是无锁状态时Mark word的拷贝,并不是引用,即使A已经获得锁,修改了Mark word的值,但是线程B中的Mark word拷贝中的值仍然是hashcode,在做CAS时,取出的线程栈桢里的拷贝值(hashcode),与对象的Mark word中的值(此时是线程A的Lock record地址)比较,必然是不一致的。

yellowFarLu commented 5 years ago

您好,我理解的轻量级锁加锁过程是: 当进行CAS替换对象头的mark world时,先判断内存中对象头mark world的值 是否等于 无锁状态的mark world,如果等于(即内存中的值 等于 预期值),则把对象头的mark world替换成指向lock record的指针。 当第二个线程到来的时候,获取内存中对象头mark world的值,该值不等于无锁状态的mark world,那么第二个线程就CAS替换失败。 因此,第二个线程无法获得锁,从而无法执行同步代码块。 这样子理解对吗?

ghost commented 4 years ago

如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态

我不明白, 偏向线程正在使用锁, 我认为偏向线程还在执行, 另一个线程想要获取锁, 为什么会变成 轻量锁? 应该变成重量锁把?

flying81621 commented 4 years ago

你觉得他会直接变成重量级锁,而不会经过尝试变成轻量级锁失败后再变成重量级锁吗?偏向线程正在执行的时候,另一个线程想获取锁,发现对象头中存了不是自己的其他线程,线程二怎么会知道线程一还在运行中?他当然是尝试变成轻量级锁,变轻量级锁失败后他才知道此时存在竞争,存在竞争说明线程一还在运行中要不然哪来的竞争,这个时候才会升级重量级锁

在 2019-12-30 21:32:54,"sc-ik" notifications@github.com 写道:

如果当前是偏向模式且偏向的线程还在使用锁,那会将锁的mark word改为轻量级锁的状态

我不明白, 偏向线程正在使用锁, 我认为偏向线程还在执行, 另一个线程想要获取锁, 为什么会变成 轻量锁? 应该变成重量锁把?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.

GarsonChan commented 4 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答 在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道: 我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? 大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

两者不一样的点就在于最后一个bit的不同。线程B通过lockee->mark()->set_unlocked()拿到的markOop,是 ” 线程A设置的markOop | unlocked_value “,也就是线程B获取到的markOop的最后一位是1(无锁状态),而线程A设置的markOop的最后一位是0(轻量级锁状态)。

Chenjiff commented 4 years ago

另外,在轻量级锁加锁中,CAS的原值若不是从对象中取,而是通过调用对象的hashcode()方法获取,那如果对象所属类的hashcode方法被重写了,获取锁的线程在代码块里头更改锁对象的某个属性值了,岂不是,当线程释放完锁之后,锁对象头里的hashcode变成了一个过期的脏数据hashcode值,那以后所有的线程都没法对这个对象加锁了?

你第一个问题是因为原值是markword被setunlock之后的(看源码),而不是直接拿的,所以最后的锁标志位会不同,第二个问题对象头的hashcode(此处学名是idendityhashcode)是不会改变的,重写hashcode方法只是对某些类例如HashMap这种显式调用有用,但是你任何时候都可以通过System.identityHashCode()获取到真正的hashcode。

wb31105 commented 4 years ago

请问为什么释放锁的时候也需要cas操作呢?为什么会有失败的情况,只有占了锁的线程才会释放啊

hwy1111 commented 4 years ago

请问为什么释放锁的时候也需要cas操作呢?为什么会有失败的情况,只有占了锁的线程才会释放啊

因为可能涉及到一个锁膨胀的过程,膨胀时要涉及到创建monitor对象,创建过程中要用到持有轻量级锁的displaced mark wrod 所以要在膨胀的过程中会设置一个INFLATING的状态标志位在对象头中(具体代码在膨胀的文中)来保证能用到 displaced mark wrod,因此CAS操作就是为这个准备的。至于为什么这样做我猜就是为了保证hashcode的一致性吧

ElvisWang commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗?

hwy1111 commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗?

没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor

iamhaoren commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗?

没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor

没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。

hwy1111 commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? 没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor

没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。

是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。

iamhaoren commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? 没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor

没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。

是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。

好的,谢谢,看了一篇文章说轻量级锁有自旋,还说的挺有道理的,哈哈。。。。。还是源码出真理啊。

hwy1111 commented 4 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? 没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor 没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。 是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。

好的,谢谢,看了一篇文章说轻量级锁有自旋,还说的挺有道理的,哈哈。。。。。还是源码出真理啊。

对的网上很多都是这样说的,我以前也是这样认为的。估计是被并发编程艺术这本书误导了,把重量级中的锁的自旋看成了轻量级也有

kaixinbaba commented 4 years ago

@flying81621 不知道你还有疑惑吗

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 然后是B线程,你的问题取决于B线程是什么时候到达synchronized的。 1.如果它和A几乎同时到达, B就会看到对象头当前处于无锁状态,B就会进行CAS去尝试替换对象的markword,那么A和B之中必定只有一个线程会成功,另一个就是失败的。 2.如果它是在A已经CAS完成后才到达的synchronized,那么B就会看到对象的markword就不是无锁状态,B就会知道当前存在并发,它接下去要做就是锁膨胀流程,这个时候B要把markword改成inflating状态就是全部都是0(同样也是使用CAS),来告诉其他线程正在进行锁膨胀。 不知道我的回答能否解决你心中的疑惑

kaixinbaba commented 4 years ago

@flying81621 不知道你还有疑惑吗

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

首先A线程通过CAS替换markword替换成功,表明A线程获取了锁,这里应该大家都没问题。 然后是B线程,你的问题取决于B线程是什么时候到达synchronized的。 1.如果它和A几乎同时到达, B就会看到对象头当前处于无锁状态,B就会进行CAS去尝试替换对象的markword,那么A和B之中必定只有一个线程会成功,另一个就是失败的。 2.如果它是在A已经CAS完成后才到达的synchronized,那么B就会看到对象的markword就不是无锁状态,B就会知道当前存在并发,它接下去要做就是锁膨胀流程,这个时候B要把markword改成inflating状态就是全部都是0(同样也是使用CAS),来告诉其他线程正在进行锁膨胀。 不知道我的回答能否解决你心中的疑惑

sshoop commented 4 years ago

为什么轻量级解锁过程要处理线程栈中所有的lock record?假设有两个重入的lock record和一个非重入的lock record,那么第一次解锁过程不应该是释放一个非重入的lock record就行了吗?

DongONNS commented 4 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

当线程B去尝试获取锁的时候,cas操作的原值不是锁对象的Mark word了,而是线程A的Lock Record的地址了,所以不能成功。

aLibeccio commented 4 years ago
// 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
highest_lock->set_displaced_header(unbiased_prototype);
obj->release_set_mark(markOopDesc::encode(highest_lock));

当线程 A 正在执行同步代码块时,线程 B 尝试获取锁失败,由 JVM 线程来执行锁撤销流程时,这里为什么是设置为无锁状态(看源码是设置最后 1 位为 1)?难道不应该是直接设置为轻量级锁的状态吗?这里我理解不是已经把连锁对象指向 lock record 的指针都设置好了的吗?

aLibeccio commented 4 years ago
// 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
highest_lock->set_displaced_header(unbiased_prototype);
obj->release_set_mark(markOopDesc::encode(highest_lock));

当线程 A 正在执行同步代码块时,线程 B 尝试获取锁失败,由 JVM 线程来执行锁撤销流程时,这里为什么是设置为无锁状态(看源码是设置最后 1 位为 1)?难道不应该是直接设置为轻量级锁的状态吗?这里我理解不是已经把连锁对象指向 lock record 的指针都设置好了的吗?

看了一下概论的图,轻量级锁指向的 displaced mark word 里的锁状态是 001(无锁)的,但是我没明白这里轻量级锁的状态什么时候改的(🤦‍♂️

aLibeccio commented 4 years ago

还有就是,如果轻量级锁加锁失败的时候,进入到 fast_enter 流程里也会让偏向锁的锁撤销计数器加一吗?因为我看一定会在 revoke_and_rebias 方法里调用到 update_heuristics 方法🤦‍♂️

aLibeccio commented 4 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答 在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道: 我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? 大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

两者不一样的点就在于最后一个bit的不同。线程B通过lockee->mark()->set_unlocked()拿到的markOop,是 ” 线程A设置的markOop | unlocked_value “,也就是线程B获取到的markOop的最后一位是1(无锁状态),而线程A设置的markOop的最后一位是0(轻量级锁状态)。

@GarsonChan 这里 set_unlocked 方法的实现是 markOop(value() | unlocked_value) 和您说的“ 线程A设置的markOop | unlocked_value”是一样,都是把最低位设置为 1

aLibeccio commented 4 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。
而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。
下面纯属个人推测:
这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。
再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

aLibeccio commented 4 years ago

@farmerjohngit 话说博主不维护这个博客了吗?好久都没看到回复了

aLibeccio commented 4 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

afreefan commented 4 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

取内存上的值,然后cas,这是两步操作,非原子的。 A线程取到内存addr上的值 a,此时A线程尚未进行cas,B 线程把addr上的值改为b, 这时A线程再CAS发现原值不是a就会失败,这不是cas的基本概念吗,你在想什么。

LilMosey commented 4 years ago

有个问题,纯小白,轻量级锁entry->lock()->set_displaced_header(displaced);,这个方法,entry是一个BasicObjectLock,对象,entry.lock返回的是一个BasicLock对象,BasicLock里面_displaced_header才是存的他的对象头的信息,但是看你的文章,一个lock record对象里面就有个markword和obj对象,有点迷。

LilMosey commented 4 years ago

我在另外一篇看到了,basiclock和basicobjectlock是lockrecord的实现,现在的新问题是,java虚拟机为什么要这么搞?openjdk里面一个lockrecord没存什么东西,除了存markoop还存了个oop指向markword,这样是不是有点多此一举了?

treycc commented 4 years ago

你还是成功绕开了我的问题,虽然你回答了不少。你回答:但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的 我的疑问一直都是:怎么会不一样? 传入的无锁状态值从什么地方取来的?难道不是从对象头中取出来?线程A已经获取了锁,现在对象头存储的是线程A的LR,线程B从对象头中取出的是线程A的LR,做CAS替换时,怎么和对象头中存储的不一样?什么叫无锁状态值?我们现在讨论的就是这个东西,你来了个糊弄的回答 在 2019-06-12 16:22:48,"scn7th" notifications@github.com 写道: 我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗? 大致明白了你的意思,你可以看下博主上面的源码,每次CAS的话,走的是Atomic::cmpxchg_ptr这个方法,对于compareValue的话,是构建一个无锁状态的Displaced Mark Word,所以线程A获得轻量级锁,修改了markword,但当线程B想要获取锁的时候,传入的无锁状态值与本身对象头存储的markword是不一样的,所以无法cas成功 我理解应该和内存可见性相关,线程A,B几乎在同时得到monitor enter指令,那么此时二者在各自线程中都存有对象O的缓存,此时必然会导致如果线程A的CAS成功,B的CAS失败 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

1.除了从偏向锁升级,轻量级锁获取的前提是无锁状态,都已经是轻量级锁还走什么偏向流程,看看monitorenter和slowenter进入CAS代码的前提是什么 2.就算真进了偏向流程,markOop displaced = lockee->mark()->set_unlocked()这句没看到么 另:别和个怨妇似的,有问题说问题,最基本的东西都不懂,还指望别人跟你解释

xdcode2020 commented 3 years ago

在撤销偏向的时候。在发现持有偏向锁的线程存活并且还在同步块中的时候。查看源码,会走下面这段核心逻辑完成锁升级的第一步, 源码位置

if (highest_lock != NULL) {
    //CODE1: 这里的unbiased_prototype是将无锁状态暂存到lockrecord中,为后续解锁恢复成无锁使用
    highest_lock->set_displaced_header(unbiased_prototype);
    //CODE2:
    obj->set_mark(markOopDesc::encode(highest_lock));
}

我的疑问:

上面CODE2之前锁状态应是偏向锁101,可CODE2看代码只是修改对象头Mark Word 中非锁状态位的值,即只修改recored指针位的值(C++代码我不太确定),但没有修改锁状态位吧?

本想做JVM的调试,可是没搞定,所以不确定这个之后Mark Word 中锁状态位是101还是001,或者其他?

我一直在找代码线索,想确定我说的这个场景(在偏向锁状态,并且偏向线程还在同步块中),当有竞争线程进入时,怎么从101变成010重量级锁的。

这里锁对象实例的锁状态过程是:101 -> 001-> 010? 还是 101->010 ? 还是其他?

因为上面这段代码感觉是我破解线索的第一步,可是刚好这步就无法确定。哎!不熟悉C++的硬伤。 求大家解惑! @farmerjohngit

soloveri commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?

DanchuoZhong commented 3 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? 没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor 没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。 是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。

好的,谢谢,看了一篇文章说轻量级锁有自旋,还说的挺有道理的,哈哈。。。。。还是源码出真理啊。

对的网上很多都是这样说的,我以前也是这样认为的。估计是被并发编程艺术这本书误导了,把重量级中的锁的自旋看成了轻量级也有

如果根本没有轻量级锁的自旋这回事的话,-XX:+UseSpinning这个指令是在什么地方用到的呢?

HenryChenV commented 3 years ago

咨询个关于自旋的问题,网上好多文章关于轻量级锁都说有个自旋的过程,当自旋尝试失败后才膨胀为重量级锁,但是从源码的角度来看,只要CAS失败就会引起锁膨胀;这块能帮忙解释一下吗? 没有自旋这回事,只有重量级锁获取失败才会自旋,网上的文章好多都是错的,我个人认为轻量级锁的意义就是在没有线程争用锁时不用创建monitor 没有自旋这回事,也就是说只要有竞争就升级为重量级锁,那和偏向锁没啥区别呀。自旋效率应该更高吧。 是的只要存在竞争就会升级重量级。轻量级锁的存在就是用于线程之间交替获取锁的场景,但是和偏向锁是有区别的啊。一个线程获取偏向锁之后,那么这个锁自然而然就属于这个线程(就算该线程释放了偏向锁也不会改变这把锁偏向这个线程的,这个前提是没有发生过批量重偏向使锁的epoch与其对应class类的epoch不相等)。所以说偏向锁的场景是用于一个线程不断的获取锁,如果把它放在轻量级锁的场景下线程之间交替获取的话会发生偏向锁的撤销的。 效率上来看偏向锁只有在获取的时候进行一次CAS,以后的释放和获取只需要简单的一些判断操作。而轻量级锁的获取和释放都要都要CAS,单纯的看效率还是偏向锁效率高。

好的,谢谢,看了一篇文章说轻量级锁有自旋,还说的挺有道理的,哈哈。。。。。还是源码出真理啊。

对的网上很多都是这样说的,我以前也是这样认为的。估计是被并发编程艺术这本书误导了,把重量级中的锁的自旋看成了轻量级也有

如果根本没有轻量级锁的自旋这回事的话,-XX:+UseSpinning这个指令是在什么地方用到的呢?

代码里没搜到,我看的jdk8u的代码

crwen commented 3 years ago

有一个疑问,在偏向锁撤销中,将所有与锁对象相关的 Lock Record 中的 Mark Word 设置为了 null,然后将第一个 Lock Record 设置为无锁状态了。

  if (highest_lock != NULL) {
    // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    ...
  }

在轻量级锁获取的过程中,也是构建了一个无锁状态的 Mark Word。在这两个位置都没有将锁对象标识为轻量级锁状态,但是在轻量级锁重入 CAS 替换 Mark Word 的时候却发现是有锁状态。请问是在哪里将锁对象标识为轻量级锁的呢

    // traditional lightweight locking
    if (!success) {
      // 构建一个无锁状态的Displaced Mark Word
      markOop displaced = lockee->mark()->set_unlocked();
      // 设置到Lock Record中去
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
        // Is it simple recursive case?
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          // CAS操作失败则调用monitorenter
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    } 
WLWL commented 3 years ago

有一个疑问,在偏向锁撤销中,将所有与锁对象相关的 Lock Record 中的 Mark Word 设置为了 null,然后将第一个 Lock Record 设置为无锁状态了。

  if (highest_lock != NULL) {
    // 修改第一个Lock Record为无锁状态,然后将obj的mark word设置为指向该Lock Record的指针
    highest_lock->set_displaced_header(unbiased_prototype);
    obj->release_set_mark(markOopDesc::encode(highest_lock));
    ...
  }

在轻量级锁获取的过程中,也是构建了一个无锁状态的 Mark Word。在这两个位置都没有将锁对象标识为轻量级锁状态,但是在轻量级锁重入 CAS 替换 Mark Word 的时候却发现是有锁状态。请问是在哪里将锁对象标识为轻量级锁的呢

    // traditional lightweight locking
    if (!success) {
      // 构建一个无锁状态的Displaced Mark Word
      markOop displaced = lockee->mark()->set_unlocked();
      // 设置到Lock Record中去
      entry->lock()->set_displaced_header(displaced);
      bool call_vm = UseHeavyMonitors;
      if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
        // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
        // Is it simple recursive case?
        if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
          entry->lock()->set_displaced_header(NULL);
        } else {
          // CAS操作失败则调用monitorenter
          CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
        }
      }
    } 

锁对象还原成无锁状态之后说明不能再偏向了(匿名偏向或者已经偏向的情况下才能继续申请偏向锁),下次对这个锁对象加锁得先从轻量级锁加起

xjzhang-97 commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

这个对齐为什么就能说明它最后两位是00了呀?求解答!!!

xjzhang-97 commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?

我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。

soloveri commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?

我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。

您的意思是说,_lock的地址必须是4的倍数?这是如何实现的?对齐如何理解啊?

xjzhang-97 commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?

我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。

您的意思是说,_lock的地址必须是4的倍数?这是如何实现的?对齐如何理解啊?

是的。_lock得是4字节对齐的。具体的可以搜索c语言4字节对齐,是和cpu内存有关的概念。

xjzhang-97 commented 3 years ago

我看了很多关于轻量级锁加锁解锁的文章,有个疑问一直没明白,包括看了你这篇。首先说CAS操作,三个要素,原值、期望值、对象,原值一定是从现对象里得到的没疑问吧?线程A加锁,CAS将锁对象对象头替换成指向线程A的Lock Record的地址,在这里,原值:对象mark word中的内容,也就是hashcode,期望值:本线程Lock Record地址,对象:锁对象,在替换成功后我们说线程A获得了锁,OK,线程A开始执行同步代码块,在它执行完之前,线程B来获取锁,发现属于轻量级锁标志,于是CAS替换mark word,此时CAS的原值仍然为为锁对象的mark word吧,而此时锁对象mark word中记录的不再是hashcode而是指向线程A的Lock Record的地址,但是对于CAS它管你对象头存的是什么,现在获取到什么,什么就是原值,于是:原值:对象头中指向线程A中LR的地址,期望值:线程B中LR(目前对他来说,是将锁对象中指向线程A中LR的地址存入本线程LR)的地址,目标对象:锁对象,怎么会CAS不成功?于是现在线程B也获取到锁,两个线程都会在执行同步代码块!这个问题你能解释下吗?希望不要用他就是这样就是那样来解释。另外,轻量级锁解锁时,除了现在锁对象已经变成了重量级锁外,解锁的CAS什么场景下会失败,能举出具体案例吗?

@flying81621 我觉得您想问的应该是下面这个 CAS 操作是怎么实现轻量级锁的互斥的。

if (!success) {
  // 构建一个无锁状态的Displaced Mark Word
  markOop displaced = lockee->mark()->set_unlocked();
  // 设置到Lock Record中去
  entry->lock()->set_displaced_header(displaced);
  bool call_vm = UseHeavyMonitors;
  if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
    // 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
    // Is it simple recursive case?
    if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
      entry->lock()->set_displaced_header(NULL);
    } else {
      // CAS操作失败则调用monitorenter
      CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
    }
  }
}

假设现在有两个线程,线程 A 首先通过锁撤销的一系列逻辑最终通过 slow_enter 方法中如下的代码把线程 A 的 Lock Record 中的 displaced header 的在线程 A 的线程栈中的地址通过 CAS 操作写进了锁对象的 mark word 中,即线程 A 成功获取了锁,继续执行同步代码块。

if (mark->is_neutral()) {
  lock->set_displaced_header(mark);
  // 请注意 CAS 操作的修改值 lock,lock 为指针类型 
  if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
    TEVENT (slow_enter: release stacklock) ;
    return ;
  }
}

我再重申一遍这个线程 A 获取轻量级锁的 CAS 操作的逻辑,把一个四字节的指针写进了锁对象的 mark word 中,所以后面取锁对象的 mark word 时会取到一个指针。 而当线程 B 执行到同步代码块准备尝试获取轻量级锁时在 CAS 操作时里用的“预期原值”为 markOop displaced = lockee->mark()->set_unlocked(),而这个构造出来的 displaced mark word 为线程 A 的线程栈里 Lock Record 中 displaced mark word 的地址“位或”上 1(具体可以看下 set_unlocked 方法的实现)使得 displaced 的最低位为 1,所以要想这个 CAS 操作失败的话(因为只有操作失败才能满足锁的互斥性),那必然 lockee->mark() 取到地址最低位应该为 0 才行。 下面纯属个人推测: 这里应该是用了某种字节对齐方式导致线程 A 中线程栈里 Lock Record 中 displaced mark word 地址的最低两位为 00,回想一下概论里的 mark word 状态转换图,在轻量级锁下最低两位为 00,而之前的 CAS 操作只写了一个地址(四字节,mark word 也是四字节),并没有专门的去改最低两位的值为 00(如果有请通知我一下,谢谢🙏,我个人找了半天没找到)。所以基于以上的方式才能实现轻量级锁的互斥。 再次重申,个人推测,错了不要骂我(要是R大来回答一下这问题就好了,唉)。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.

// Because some machines have alignment restrictions on the control stack,
// the actual space allocated by the interpreter may include padding words
// after the end of the BasicObjectLock.  Also, in order to guarantee
// alignment of the embedded BasicLock objects on such machines, we
// put the embedded BasicLock at the beginning of the struct.

class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
  friend class VMStructs;
 private:
  BasicLock _lock;                                    // the lock, must be double word aligned
  oop       _obj;                                          // object holds the lock;

仔细看了一下 BasicObjectLock 的注释,确实因为这个原因

你好,请问这个对齐原因和锁状态被设置为00有什么关系呢?

我悟了。因为双字对齐,也就是4字节对齐,一个地址4字节对齐必须能被4整除。所以最后两位都是0,才能4字节对齐。

您的意思是说,_lock的地址必须是4的倍数?这是如何实现的?对齐如何理解啊?

是的。4字节对齐要求,起始地址是4字节对齐,整个结构体长度要4字节对齐。这个4字节对齐是为了方便cpu读取内存数据,具体的我也不知道。