文档章节

一文让你读懂Synchronized底层实现,秒杀面试官

李红欧巴
 李红欧巴
发布于 08/07 15:33
字数 2015
阅读 8
收藏 0

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

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

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

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

本文分为两个部分:

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 Record的 Displaced 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

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

为什么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方法,相关逻辑将在重量级锁的文章中讲解。

 

本人免费整理了Java高级资料,涵盖了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高并发分布式等教程,一共30G,需要自己领取。
传送门:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

© 著作权归作者所有

李红欧巴

李红欧巴

粉丝 52
博文 152
码字总数 498454
作品 0
长沙
私信 提问
Java面试,你应该准备这些知识

点击上方“程序员小灰”,选择“置顶公众号” 有趣有内涵的文章第一时间送达! 本来转载自公众号 占小狼的博客 作者占小狼 来自 美团点评 基础架构组 width="auto" src="http://ss.csdn.net...

bjweimengshu
2018/01/09
0
0
程序员周末阿里面试,5分钟就被一道题秒杀:HashMap与Hashtable

你们可能会想,我这么菜的吗?5分钟都坚持不了? 本文说起来会有点尴尬,毕竟这是我曾经经历过的故事 那时候的我还真菜,每天写着 if/ for 及一些简单的业务逻辑代码,虽工作有些日子了,但技...

我最喜欢三大框架
03/17
67
0
BAT等公司必问的8道Java经典面试题,你都会了吗?

工作多年以及在面试中,我经常能体会到,有些面试者确实是认真努力工作,但坦白说表现出的能力水平却不足以通过面试,通常是两方面原因: 1、“知其然不知其所以然”。做了多年技术,开发了很...

java填坑路
01/06
0
0
Java学到什么程度可以去找工作及面试流程

一、简历 简历里面需要包含的内容应该是学历,自己的技术栈,然后自己做过的一些项目。简历不需要太长,两页纸即可。里面应该重点写你使用过的一些框架,自己做的一些项目,以及自己的收获,...

野梦M
2017/11/30
0
0
【鲁班学院】一个三年工作经验和月薪16k的java程序员应该要具备什么样的技能?

大家好,我是鲁班学院的阿琰!我是有着三年工作经验的Java程序员。 目前一线城市的薪资水平在13到16K之间,但是找工作的时候并不仅仅有工作经验就行了,还需要一定的专业知识。如果你野心更大...

骚年锦时
05/30
85
0

没有更多内容

加载失败,请刷新页面

加载更多

使用原生css+js+html实现打印A4纸张的功能页面

有时候我们需要使用html+css实现打印A4纸张的功能页面,以下代码实现 <!DOCTYPE html><html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatibl......

b0cloud
20分钟前
2
0
读组件化之MGJRouter源码第二次的收获与思考

上一次我们写好了一个自定义的 路由类 ,然后我们来制作自己的 库 ,可以用来被 pod 引入 : 库的制作参考:https://www.jianshu.com/p/928d2ab053be 以下是我创建的: 利用上篇提到的 ,组件...

T型人才追梦者
21分钟前
1
0
spring cache、ehcache的使用及集成

项目中需要加缓存,故学习了 1、spring cache、ehcache的使用及集成 2、缓存的命中率等统计数据 一、spring cache 1、概述 Spring 3.1 引入了基于注解(annotation)的缓存(cache)技术 2、...

qkKing
22分钟前
3
0
Windows 10上源码编译Poco并编写httpserver和tcpserver | compile and install poco cpp library on windows

本文首发于个人博客https://kezunlin.me/post/9587bb47/,欢迎阅读! compile and install poco cpp library on windows Series guide to compile and install poco cpp library on windows g......

kezunlin
23分钟前
2
0
if-else-if-else与switch的区别

if-else-if-else: 适合分支较少 判断条件类型不单一 支持取 boolean 类型的所有运算 满足条件即停止对后续分支语句的执行 switch: 适合分支较多 判断条件类型单一,JDK 1.7 之前仅支持 in...

ConstXiong
23分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部