Java 如何不使用 volatile 和锁实现共享变量的同步操作

news2025/3/3 4:34:16

前言

熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。

按照官方的说法:

当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有 HB 关系,则会产生数据竞争问题。

要想保证操作 B 的线程看到操作 A 的结果(无论 AB 是否在一个线程),那么在 AB 之间必须满足 HB 原则,如果没有,将有可能导致重排序。

当缺少 HB 关系时,就可能出现重排序问题。

HB 有哪些规则?

这个大家都非常熟悉了应该,大部分书籍和文章都会介绍,这里稍微回顾一下:

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2.锁定规则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

其中,传递规则我加粗了,这个规则至关重要。如何熟练的使用传递规则是实现同步的关键

然后,再换个角度解释 HB:当一个操作 A HB 操作 B,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

同时,如果 操作 B HB 操作 C,那么,操作 A 对共享变量的操作结果对操作 B 都是可见的。

而实现可见性的原理则是 cache protocol 和 memory barrier。通过缓存一致性协议和内存屏障实现可见性。

如何实现同步?

在 Doug Lea 著作 《Java Concurrency in Practice》中,有下面的描述:

书中提到:通过组合 hb 的一些规则,可以实现对某个未被锁保护变量的可见性。

但由于这个技术对语句的顺序很敏感,因此容易出错

楼主接下来,将演示如何通过 volatile 规则和程序次序规则实现对一个变量同步。

来一个熟悉的例子:

class ThreadPrintDemo {static int num = 0;static volatile boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {for (; 100 > num; ) {if (!flag && (num == 0 || ++num % 2 == 0)) {System.out.println(num);flag = true;}}});Thread t2 = new Thread(() -> {for (; 100 > num; ) {if (flag && (++num % 2 != 0)) {System.out.println(num);flag = false;}}});t1.start();t2.start();}
} 

这段代码的作用是两个线程间隔打印出 0 - 100 的数字。

熟悉并发编程的同学肯定要说了,这个 num 变量没有使用 volatile,会有可见性问题,即:t1 线程更新了 num,t2 线程无法感知。

哈哈,楼主刚开始也是这么认为的,但最近通过研究 HB 规则,我发现,去掉 num 的 volatile 修饰也是可以的。

我们分析一下,楼主画了一个图:

我们分析这个图:

1.首先,红色和黄色表示不同的线程操作。
2.红色线程对 num 变量做 ++,然后修改了 volatile 变量,这个是符合 程序次序规则的。也就是 1 HB 2.
3.红色线程对 volatile 的写 HB 黄色线程对 volatile 的读,也就是 2 HB 3.
4.黄色线程读取 volatile 变量,然后对 num 变量做 ++,符合 程序次序规则,也就是 3 HB 4.
5.根据传递性规则,1 肯定 HB 4. 所以,1 的修改对 4来说都是可见的。

注意:HB 规则保证上一个操作的结果对下一个操作都是可见的。

所以,上面的小程序中,线程 A 对 num 的修改,线程 B 是完全感知的 —— 即使 num 没有使用 volatile 修饰。

这样,我们就借助 HB 原则实现了对一个变量的同步操作,也就是在多线程环境中,保证了并发修改共享变量的安全性。并且没有对这个变量使用 Java 的原语:volatile 和 synchronized 和 CAS(假设算的话)。

这可能看起来不安全(实际上安全),也好像不太容易理解。因为这一切都是 HB 底层的 cache protocol 和 memory barrier 实现的。

其他规则实现同步

1.利用线程终结规则实现:

 static int a = 1;public static void main(String[] args) {Thread tb = new Thread(() -> {a = 2;});Thread ta = new Thread(() -> {try {tb.join();} catch (InterruptedException e) {//NO}System.out.println(a);});ta.start();tb.start();} 

2.利用线程 start 规则实现:

 static int a = 1;public static void main(String[] args) {Thread tb = new Thread(() -> {System.out.println(a);});Thread ta = new Thread(() -> {a = 2;tb.start();});ta.start();} 

这两个操作,也可以保证变量 a 的可见性。

确实有点颠覆之前的观念。之前的观念中,如果一个变量没有被 volatile 修饰或 final 修饰,那么他在多线程下的读写肯定是不安全的 —— 因为会有缓存,导致读取到的不是最新的。

然而,通过借助 HB,我们可以实现。

总结

虽然本文标题是通过 happen-before 实现对共享变量的同步操作,但主要目的还是更深刻的理解 happen-before,理解他的 happen-before 概念其实就是保证多线程环境中,上一个操作对下一个操作的有序性和操作结果的可见性。

同时,通过灵活的使用传递性规则,再对规则进行组合,就可以将两个线程进行同步 —— 实现指定的共享变量不使用原语也可以保证可见性。虽然这好像不是很易读,但也是一种尝试。

关于如何组合使用规则实现同步,Doug Lea 在 JUC 中给出了实践。

例如老版本的 FutureTask 的内部类 Sync(已消失),通过 tryReleaseShared 方法修改 volatile 变量,tryAcquireShared 读取 volatile 变量,这是利用了 volatile 规则;

通过在 tryReleaseShared 之前设置非 volatile 的 result 变量,然后在 tryAcquireShared 之后读取 result 变量,这是利用了程序次序规则。

从而保证 result 变量的可见性。和我们的第一个例子类似:利用程序次序规则和 volatile 规则实现普通变量可见性。

而 Doug Lea 自己也说了,这个“借助”技术非常容易出错,要谨慎使用。但在某些情况下,这种“借助”是非常合理的。

实际上,BlockingQueue 也是“借助”了 happen-before 的规则。还记得 unlock 规则吗?当 unlock 发生后,内部元素一定是可见的。

而类库中还有其他的操作也“借助”了 happen-before 原则:并发容器,CountDownLatch,Semaphore,Future,Executor,CyclicBarrier,Exchanger 等。

总而言之,言而总之:

happen-before 原则是 JMM 的核心所在,只有满足了 hb 原则才能保证有序性和可见性,否则编译器将会对代码重排序。hb 甚至将 lock 和 volatile 也定义了规则。

通过适当的对 hb 规则的组合,可以实现对普通共享变量的正确使用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

「数据密集型系统搭建」原理篇|数据类型不怕精挑细选

本篇围绕MySQL数据库的底层存储模型、列类型来聊聊数据库表设计及建模中要注意的事项,剖析最根源的底层物理存储文件,用最真实的数据剖析来证明和解答开发过程中的疑惑。 在一些技术谈资、面试沟通过程中,MySQL特别是我们常用的Innodb存储引擎…

JavaScript 作用域

文章目录JavaScript 作用域JavaScript 作用域JavaScript 局部作用域JavaScript 全局变量JavaScript 变量生命周期函数参数HTML 中的全局变量你知道吗?JavaScript 作用域 作用域可访问变量的集合。 JavaScript 作用域 在 JavaScript 中, 对象和函数同样也是变量。 在 JavaScr…

ONES X 海银财富|以敏捷流程管理,创新金融服务平台

近日,ONES 签约财富管理行业领跑者——海银财富,助力海银财富落地敏捷流程管理,打造从需求到交付的一体化平台,快速接受业务方的反馈,进行金融平台的迭代与优化。海银财富管理有限公司(以下简称海银财富&am…

拆机详解:1968年军用集成电路计算机 高级货赢在做工

halo大家好,这里是一天更两篇的Eric。 今天我在网上偶然看到一个拆军用计算机的,正好给你们分享一下。这可是1970年左右为了F4战斗机敌我识别系统打造的,虽说比之前说的Macintosh更加的挤也更大,不过做工够扎实。 上图&#xff…

centos8安装RabbitMQ和erlang

RabbitMQ 消息队列MQ RabbitMQ简称MQ是一套实现了高级消息队列协议的开源消息代理软件,简单来说就是一个消息中间件。是一种程序对程序的通信方法,其服务器也是以高性能、健壮以及可伸缩性出名的Erlang语言编写而成为什么使用MQ 在项目中,…

QSyntaxHighlighter

一、描述 此类用于自定义语法高亮显示规则,是用于实现 QTextDocument 文本高亮显示的基类。 要自定义语法高亮显示,必须子类化 QSyntaxHighlighter 并重新实现 highlightBlock()。此函数将在合适的时机自动被调用。 highlightBlock() 函数将格式设置应…

SOFA Weekly|SOFANews、本周贡献 issue 精选

SOFA WEEKLY | 每周精选 筛选每周精华问答,同步开源进展欢迎留言互动~SOFAStack(Scalable Open Financial Architecture Stack)是蚂蚁集团自主研发的金融级云原生架构,包含了构建金融级云原生架构所需的各个组件&#…

Rasa 3.x 学习系列-Rasa [3.4.0] - 2022-12-14新版本发布

Rasa 3.x 学习系列-Rasa [3.4.0] - 2022-12-14新版本发布 任何人都可以学习Rasa之优秀Rasa学习资源推荐 欢迎同学们报名Gavin老师的Rasa系列课程,任何人都可以学习Rasa之优秀Rasa学习资源推荐: 1.NLP on Transformers高手之路137课 2 .Rasa 3.X 智能对话机器人案例开发硬核…

五、k8s pod详解

文章目录1 pod介绍1.1 pod 定义2 pod配置2.1 基本配置2.2 镜像拉取2.3 启动命令2.4 环境变量2.5 端口设置2.6 资源配额3 Pod生命周期3.1 创建和终止3.2 初始化容器3.3 钩子函数3.4 容器探测3.5 重启策略4 Pod调度4.1 定向调度4.2 亲和性调度5 污点和容忍5.1 污点(Ta…

做短视频必须了解的6个问题,你知道几个答案呢?

做短视频必须了解的6个问题,你知道几个答案呢? 最近好多朋友问了视频运营和创作的问题,把其中六个有代表性的问题和答案汇总在一起,公开给大家,希望对大家有所帮助。 1、账号被限流了怎么办? 随手拍十条…

聚观早报 | 小米同时研发两款车;谷歌计划向印度最高法院上诉

小米同时研发两款车:谷歌计划向印度最高法院上诉;苹果AR/MR头显部件延迟发货;2022年特斯拉在德汽车销量激增;纽约市教育部门禁止访问 ChatGPT 小米同时研发两款车小米第一款车为中型溜背式轿车(内部代号 Modena 摩德纳…

欧科云链任煜男:推动区块链创新,切忌陷入“过度金融化”的桎梏

FTX破产轰动全球,揭示的是监管出现问题。FTX作为行业龙头,持有多国牌照却依然“暴雷”,未来到底应如何监管加密资产行业?针对这一问题,欧科云链控股(01499.HK)公司执行董事、董事局主席兼行政总裁任煜男近日在接受香港…

【菜菜的CV进阶之路 - 深度学习环境搭建】windows+ubuntu20.04双系统安装

新学期,配了台新电脑(双路2080ti5800x64GB内存500GB固态2TB机械),师兄忙于毕设,没给装ubuntu,自己装一下咯~ 一、前期准备 1、空U盘一个 2、Ubuntu 20.04 LTS镜像 3、启动U盘制作软件(我用的…

STM51嵌入式开发入门软件安装-Keil、stc、驱动

STM51嵌入式开发入门软件安装-Keil、stc、驱动 STM51嵌入式开发入门软件安装-Keil、stc、驱动 1 Keil安装 1.1 百度进入官网 1.2 选择C51,点击后需要填的信息随便填 下载好的软件链接:https://pan.baidu.com/s/19wxkLD69QpmPNr27KYRAxw?pwdyihl 提取码&#xff1a…

【web】微信小程序笔记小结(简介)

准备参加计算机设计大赛owo 疯狂学习微信小程序ing js也在努力兼顾kkk 写完想搞个目录结果老是跳转有问题orz,所以这一篇没有目录了qwq 来源:黑马程序员前端微信小程序开发教程 I. 小程序与普通网页的区别 ① 运行环境 网页 → 浏览器环境 小…

自定义指令binding参数

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作 如果想注册局部指令&#…

自定义viewGroup

案例分析:继承自extends LinearLayoutpublic class MainTabBarView extends LinearLayout实现3个方法public MainTabBarView(Context context) {this(context, null);}public MainTabBarView(Context context, Nullable AttributeSet attrs) {this(context, attrs, …

Kettle--MySQL生产数据库千万、亿级数据量迁移方案及性能优化

一、Kettle环境搭建 一、Windows主要用于可视化创建数据迁移用到的ktr转换脚本、kjb作业脚本,以及脚本需要配置的DB2信息、读写性能优化信息等,也可直接在客户端完成不同数据库之间的数据迁移。 测试库表及数据 (1)创建测试库表…

PVE安装win10并开启远程桌面

接上一篇一、win10安装镜像最新版下载下载地址:https://next.itellyou.cn/现在的win10最新版时22h2,文件名为zh-cn_windows_10_business_editions_version_22h2_updated_dec_2022_x64_dvd_cde06342.iso如果对更新有要求,建议每月跟踪下载一次二、上传win…

IRCNN-FPOCS 代码解读(1):整体框架

0 前言 按照自己实现论文代码的思路,去研究作者的代码,找到自己的知识盲区和不足,提升编码技能。 本模块主要介绍代码实现思路。细节分析详见后续博客。 1、合成地震数据 利用波动方程???合成数据&…