什么是synchronized的重量级锁

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

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

获取重量级锁

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

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

  • 关于线程你必须知道的8个问题(中)中提到过该方法;
  • 问题是锁升级(膨胀),但重点不在ObjectSynchronizer::inflate,因此代码分析放在重量级锁源码分析中。

锁的结构

了解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

  • 适应性自旋的原码分析放在了重量级锁源码分析中;
  • objectMonitor#TryLock非常简单,关键技术依旧是CAS。

互斥的实现

到目前为止,无论是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 (;;) {// 尝试设置_Responsibleif ((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个问题(中)聊到过park和parkEvent,底层是通过pthread_cond_wait和pthread_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指令执行前,对所有处理器可见。

Tips:volatile中详细解释内存屏障。

唤醒的策略

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

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方法,这里就不多说了。

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

总结

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

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

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


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/88194.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云计算——虚拟化中的网络架构与虚拟网络(文末送书)

作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号:网络豆 座右铭:低头赶路,敬事如仪 个人主页: 网络豆的主页​​​​​ 目录 前期回顾 前言 一.网卡虚拟化 1.网卡虚拟化方法&…

海康威视相机-LINUX SDK 开发

硬件与环境 相机: MV-CS020-10GC 系统:UBUNTU 22.04 语言:C 工具:cmake 海康官网下载SDK 运行下面的命令进行安装 sudo dpkg -i MVSXXX.deb安装完成后从在/opt/MVS 路径下就有了相关的库,实际上我们开发的时候只需要…

CausalEGM:通过编码生成建模的通用因果推理框架

英文题目:CausalEGM: a general causal inference framework by encoding generative modeling 中文题目:CausalEGM:通过编码生成建模的通用因果推理框架 单位:斯坦福大学统计系 时间:2023 论文链接:ht…

ceph对象三元素data、xattr、omap

这里有一个ceph的原则,就是所有存储的不管是块设备、对象存储、文件存储最后都转化成了底层的对象object,这个object包含3个元素data,xattr,omap。data是保存对象的数据,xattr是保存对象的扩展属性,每个对象…

java+ssm+mysql农场信息管理系统

项目介绍: 本系统为基于jspssmmysql的农场信息管理系统,功能如下: 用户:注册登录系统,菜地信息管理,农作物信息管理,种植信息管理,客户信息管理,商家信息管理&#xff…

BingChat与ChatGPT比较,哪个聊天机器人能让你获益更多?

人工智能领域的最新进展为普通人创造新的收入来源提供了更多机会。今年早些时候,微软对OpenAI进行了大量投资。此后,微软在Microsoft Edge浏览器中推出了自家的聊天机器人Bing Chat。 在论坛和社交媒体上,你可以发现这两个AI工具都吸引了很…

Property ‘sqlSessionFactory‘ or ‘sqlSessionTemplate‘ are required

项目场景: 最近因为公司业务需要在搭一个新架构,用的springboot3和jdk17,在整合mybatis多数据源的时候报错 (引用的mybatisplus 和 mybatisplusjion的是最新的包-2023-08-26) Error creating bean with name ‘XXXServiceImpl’:…

[已解决] wget命令出现Unable to establish SSL connection.错误

问题 从win11上下载best.ckpt包时 遇到: Unable to establish SSL connection.错误 解决方案: 加上参数: 加上跳过验证证书的参数--no-check-certificate 有些网站不允许通过非浏览器的方式进行下载,使用代理既可以解决问题&am…

【C++入门】模版初阶(泛型编程)

目录 1.泛型编程2.函数模版2.1函数模版的概念2.2函数模版的使用2.3函数模版的原理2.4函数模版的实例化2.5 模板参数的匹配原则 3.类模版3.1类模版的定义格式3.2类模版的实例化 1.泛型编程 让我们思考一个小问题:如何实现一个通用的交换函数呢? 在解决这…

构建个人博客_Obsidian_github.io_hexo

1 初衷 很早就开始分享文档,以技术类的为主,一开始是 MSN,博客,随着平台的更替,后来又用了 CSDN,知乎,简书…… 再后来是 Obsidian,飞书,Notion,常常有以下困…

ElasticSearch基础知识汇总

文章目录 前言一、认识ElasticSearch1.正向索引和倒排索引2. MySql与ElasticSearc3.IK分词器 二、ES索引库操作1.mapping映射属性2.索引库的CRUD 三、ES文档库操作 前言 Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基…

详解过滤器Filter和拦截器Interceptor的区别和联系

目录 前言 区别 联系 前言 过滤器(Filter)和拦截器(Interceptor)都是用于在Web应用程序中处理请求和响应的组件,但它们在实现方式和功能上有一些区别。 区别 1. 实现方式: - 过滤器是基于Servlet规范的组件,通过实现javax.servlet.Filt…