大家好,我是王有志。关注王有志,一起聊技术,聊游戏,聊在外漂泊的生活。

今天我们继续学习synchronized的升级过程,目前只剩下最后一步了:轻量级锁->重量级锁。

通过今天的内容,希望能帮助大家解答synchronized都问啥?中除锁粗化,锁消除以及Java 8对synchronized的优化外全部的问题。

获取重量级锁

从源码揭秘偏向锁的升级最后,看到synchronizer#slow_enter如果存在竞争,会调用ObjectSynchronizer::inflate方法,进行轻量级锁的升级(膨胀)。

Tips

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
......
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

通过ObjectSynchronizer::inflate获取重量级锁ObjectMonitor,然后执行ObjectMonitor::enter方法。

Tips

锁的结构

了解ObjectMonitor::enter的逻辑前,先来看ObjectMonitor的结构:

class ObjectMonitor {
private:
// 保存与ObjectMonitor关联Object的markOop
volatile markOop _header; // 与ObjectMonitor关联的Object
void* volatile _object;
protected: // ObjectMonitor的拥有者
void *  volatile _owner; // 递归计数
volatile intptr_t  _recursions; // 等待线程队列,cxq移入/Object.notify唤醒的线程
ObjectWaiter * volatile _EntryList;
private: // 竞争队列
ObjectWaiter * volatile _cxq; // ObjectMonitor的维护线程
Thread * volatile _Responsible;
protected: // 线程挂起队列(调用Object.wait)
ObjectWaiter * volatile _WaitSet;
}

_header字段存储了Object的markOop,为什么要这样?因为锁升级后没有空间存储Object的markOop了,存储到_header中是为了在退出时能够恢复到加锁前的状态

Tips

  • 实际上basicLock也存储了对象的markOop;
  • EntryList中等待线程来自于cxq移入,或Object.notify唤醒但未执行。

重入的实现

objectMonito#enter方法可以拆成三个部分,首先是竞争成功或重入的场景

// 获取当前线程Self
Thread * const Self = THREAD; // CAS抢占锁,如果失败则返回_owner
void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
if (cur == NULL) {
// CAS抢占锁成功直接返回
return;
} // CAS失败场景
// 重量级锁重入
if (cur == Self) {
// 递归计数+1
_recursions++;
return;
} // 当前线程是否曾持有轻量级锁
// 可以看做是特殊的重入
if (Self->is_lock_owned ((address)cur)) {
// 递归计数器置为1
_recursions = 1;
_owner = Self;
return;
}

重入和升级的场景中,都会操作_recursions_recursions记录了进入ObjectMonitor的次数,解锁时要经历相应次数的退出操作才能完成解锁。

适应性自旋

以上都是成功获取锁的场景,那么产生竞争导致失败的场景是怎样的呢?来看适应性自旋的部分,ObjectMonitor倒数第二次对“轻量”的追求

// 尝试自旋来竞争锁
Self->_Stalled = intptr_t(this);
if (Knob_SpinEarly && TrySpin (Self) > 0) {
Self->_Stalled = 0;
return;
}

objectMonitor#TrySpin方法是对适应性自旋的支持。Java 1.6后加入,移除默认次数的自旋,将自旋次数的决定权交给JVM。

JVM根据锁上一次自旋情况决定,如果刚刚自旋成功,并且持有锁的线程正在执行,JVM会允许再次尝试自旋。如果该锁的自旋经常失败,那么JVM会直接跳过自旋过程

Tips

互斥的实现

到目前为止,无论是CAS还是自旋,都是偏向锁和轻量级锁中出现过的技术,为什么会让ObjectMonitor背上“重量级”的名声呢?

最后是竞争失败的场景:

// 此处省略了修改当前线程状态的代码
for (;;) {
EnterI(THREAD);
}

实际上,进入ObjectMonitor#EnterI后也是先尝试“轻量级”的加锁方式:

void ObjectMonitor::EnterI(TRAPS) {
if (TryLock (Self) > 0) {
return;
} if (TrySpin (Self) > 0) {
return;
}
}

接来下是重量级的真正实现:

// 将当前线程(Self)封装为ObjectWaiter的node
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev   = (ObjectWaiter *) 0xBAD;
node.TState  = ObjectWaiter::TS_CXQ; // 将node插入到cxq的头部
ObjectWaiter * nxt;
for (;;) {
node._next = nxt = _cxq;
if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt)
break; // 为了减少插入到cxq头部的次数,试试能否直接获取到锁
if (TryLock (Self) > 0) {
return;
}
}

逻辑一目了然,封装ObjectWaiter对象,并加入到cxq队列头部。接着往下执行:

// 将当前线程(Self)设置为当前ObjectMonitor的维护线程(_Responsible)
// SyncFlags的默认值为0,可以通过-XX:SyncFlags设置
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
Atomic::replace_if_null(Self, &_Responsible);
} for (;;) {
// 尝试设置_Responsible
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::replace_if_null(Self, &_Responsible);
}
// park当前线程
if (_Responsible == Self || (SyncFlags & 1)) {
Self->_ParkEvent->park((jlong) recheckInterval);
// 简单的退避算法,recheckInterval从1ms开始
recheckInterval *= 8;
if (recheckInterval > MAX_RECHECK_INTERVAL) {
recheckInterval = MAX_RECHECK_INTERVAL;
}
} else {
Self->_ParkEvent->park();
} // 尝试获取锁
if (TryLock(Self) > 0)
break;
if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0)
break; if (_succ == Self)
_succ = NULL;
}

逻辑也不复杂,不断的park当前线程,被唤醒后尝试获取锁。需要关注-XX:SyncFlags的设置:

  • SyncFlags == 0时,synchronized直接挂起线程;
  • SyncFlags == 1时,synchronized将线程挂起指定时间。

前者是永久挂起,需要被其它线程唤醒,而后者挂起指定的时间后自动唤醒

Tips关于线程你必须知道的8个问题(中)聊到过parkparkEvent,底层是通过pthread_cond_waitpthread_cond_timedwait实现的。

释放重量级锁

释放重量级锁的源码和注释非常长,我们省略大部分内容,只看关键部分。

重入锁退出

我们知道,重入是不断增加_recursions的计数,那么退出重入的场景就非常简单了:

void ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * const Self = THREAD; // 第二次持有锁时,_recursions == 1
// 重入场景只需要退出重入即可
if (_recursions != 0) {
_recursions--;
return;
}
.....
}

不断的减少_recursions的计数。

释放和写入

JVM的实现中,当前线程是锁的持有者且没有重入时,首先会释放自己持有的锁,接着将改动写入到内存中,最后还肩负着唤醒下一个线程的责任。先来看释放和写入内存的逻辑:

// 置空锁的持有者
OrderAccess::release_store(&_owner, (void*)NULL); // storeload屏障,
OrderAccess::storeload(); // 没有竞争线程则直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT(Inflated exit - simple egress);
return;
}

storeload屏障,对于如下语句:

store1;
storeLoad;
load2

保证store1指令的写入在load2指令执行前,对所有处理器可见。

Tipsvolatile中详细解释内存屏障。

唤醒的策略

执行释放锁和写入内存后,只需要唤醒下一个线程来“交接”锁的使用权。但是有两个“等待队列”:cxqEntryList,该从哪个开始唤醒呢?

Java 11前,根据QMode来选择不同的策略:

  • QMode == 0,默认策略,将cxq放入EntryList
  • QMode == 1,翻转cxq,并放入EntryList
  • QMode == 2,直接从cxq中唤醒;
  • QMode == 3,将cxq移入到EntryList的尾部;
  • QMode == 4,将cxq移入到EntryList的头部。

不同的策略导致了不同的唤醒顺序,现在你知道为什么说synchronized是非公平锁了吧?

objectMonitor#ExitEpilog方法就很简单了,调用的是与park对应的unpark方法,这里就不多说了。

TipsJava 12的objectMonitor移除了QMode,也就是说只有一种唤醒策略了。

总结

我们对重量级锁做个总结。synchronized的重量级锁是ObjectMonitor,它使用到的关键技术有CAS和park。相较于mutex#Monitor来说,它们的本质相同,对park的封装,但ObjectMonitor是做了大量优化的复杂实现。

我们看到了重量级锁是如何实现重入性的,以及唤醒策略导致的“不公平”。那么我们常说的synchronized保证了原子性,有序性和可见性,是如何实现的呢?

大家可以先思考下这个问题,下篇文章会做一个全方位的总结,给synchronized收下尾。


好了,今天就到这里了,Bye~~

09.什么是synchronized的重量级锁?的更多相关文章

  1. 再有人说synchronized是重量级锁,就把这篇文章扔给他看

    synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用. 但不可否认的是synchr ...

  2. 重量级锁synchronized的优化----自旋锁、自适应自旋锁、锁消除、锁粗化

    synchronized是重量级锁,效率不高.但在jdk 1.6中对synchronize的实现进行了各种优化,使得它显得不是那么重了.jdk1.6对锁的实现引入了大量的优化,如自旋锁.自适应自旋锁. ...

  3. Java多线程系列 基础篇06 synchronized(同步锁)

    转载 http://www.cnblogs.com/paddix/ 作者:liuxiaopeng http://www.infoq.com/cn/articles/java-se-16-synchro ...

  4. synchronized凭什么锁得住?

    相关链接: <synchronized锁住的是谁?> 我们知道synchronized是重量级锁,我们知道synchronized锁住的是一个对象上的Monitor对象,我们也知道sync ...

  5. 【转载】Java中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景 & AtomicReference

    参考文章: http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接 ...

  6. java并发笔记之synchronized 偏向锁 轻量级锁 重量级锁证明

    警告⚠️:本文耗时很长,先做好心理准备 本篇将从hotspot源码(64 bits)入手,通过分析java对象头引申出锁的状态:本文采用大量实例及分析,请耐心看完,谢谢   先来看一下hotspot的 ...

  7. 线程安全(中)--彻底搞懂synchronized(从偏向锁到重量级锁)

    接触过线程安全的同学想必都使用过synchronized这个关键字,在java同步代码快中,synchronized的使用方式无非有两个: 通过对一个对象进行加锁来实现同步,如下面代码. synchr ...

  8. synchronized底层实现原理&CAS操作&偏向锁、轻量级锁,重量级锁、自旋锁、自适应自旋锁、锁消除、锁粗化

    进入时:monitorenter 每个对象有一个监视器锁(monitor).当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:1 ...

  9. java 偏向锁、轻量级锁及重量级锁synchronized原理

    Java对象头与Monitor java对象头是实现synchronized的锁对象的基础,synchronized使用的锁对象是存储在Java对象头里的. 对象头包含两部分:Mark Word 和 ...

  10. Synchronized机制下偏向锁、轻量级锁、重量级锁的适用场景

    上次总结了Synchronized机制下的锁的种类和原理,这次总结一下几种锁的适用场景. 偏向锁 一个线程获取某个对象的偏向锁的成本是很低的,只需把对象头的偏向线程id改为自己就好,如果偏向线程id已 ...

随机推荐

  1. 华为路由器NAT基本配置命令

    NAT地址转换 静态 [R1]int g0/0/0 [R1-GigabitEthernet0/0/0]nat static global 202.169.10.5 inside 172.16.1.1 ...

  2. SpringBoot框架SpEL表达式注入漏洞复现与原理分析

    前言 这是2016年的一个洞,利用条件是至少知道一个触发 springboot 默认错误页面的接口及参数名. 影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升级版本 ...

  3. 2流高手速成记(之四):SpringBoot整合redis及mongodb

    最近很忙,好不容易才抽出了时间,咱们接上回 上次我们主要讲了如何通过SpringBoot快速集成mybatis/mybatis-plus,以实现业务交互中的数据持久化,而这一切都是基于关系型数据库(S ...

  4. 第三方库openPyxl读取excel文件

    import openpyxl from openpyxl.worksheet.worksheet import Worksheet def openpyxl_read(): #1.打开文件 work ...

  5. 6.channels 配置websocket

      Django默认不支持websockey,需要Django支持的话需要安装第三方组件 django channels 是django支持websocket的一个模块.   1.安装 pip3 in ...

  6. jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一)

    jdk线程池ThreadPoolExecutor工作原理解析(自己动手实现线程池)(一) 线程池介绍 在日常开发中经常会遇到需要使用其它线程将大量任务异步处理的场景(异步化以及提升系统的吞吐量),而在 ...

  7. SpringBoot怎么自定义一个Starter ?

    小伙伴们曾经可能都经历过整天写着CURD的业务,都没写过一些组件相关的东西,这篇文章记录一下SpringBoot如何自定义一个Starter. 原理和理论就不用多说了,可以在网上找到很多关于该方面的资 ...

  8. Go语言核心36讲37

    你好,我是郝林,今天我们继续来分享并发安全字典sync.Map的内容. 我们在上一篇文章中谈到了,由于并发安全字典提供的方法涉及的键和值的类型都是interface{},所以我们在调用这些方法的时候, ...

  9. .NET跨平台框架选择之一 - Avalonia UI

    本文阅读目录 1. Avalonia UI简介 Avalonia UI文档教程:https://docs.avaloniaui.net/docs/getting-started 随着跨平台越来越流行, ...

  10. UBOOT编译--- UBOOT编译过程目标依赖分析(八)

    1. 前言 UBOOT版本:uboot2018.03,开发板myimx8mmek240. 2. 概述 本文采用自顶向下的方法,从顶层目标开始到最原始的依赖. 3. u-boot编译 uboot的编译分 ...