java 并发编程之共享变量的实现方法

可见性

如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性.

Java 虚拟机规范试图定义一种 Java 内存模型 (JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台上都能达到一致的内存访问效果.

简单来说,由于 CPU 执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存.

在 Java 内存模型里,对上述的优化又进行了一波抽象. JMM 规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成 CPU 上的寄存器或者高速缓存.

所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存.

简单点就是,多线程中读取或修改共享变量时,首先会读取这个变量到自己的工作内存中成为一个副本,对这个副本进行改动后,再更新回主内存中.

java 并发编程之共享变量的实现方法


使用工作内存和主存,虽然加快的速度,但是也带来了一些问题. 比如看下面一个例子:

i = i + 1;

假设 i 初值为 0,当只有一个线程执行它时,结果肯定得到 1,当两个线程执行时,会得到结果 2 吗? 这倒不一定了. 可能存在这种情况:

线程1: load i from 主存  // i = 0
    i + 1 // i = 1
线程2: load i from主存 // 因为线程1还没将i的值写回主内存,所以i还是0
    i + 1 //i = 1
线程1: save i to 主存
线程2: save i to 主存

如果两个线程按照上面的执行流程,那么 i 最后的值居然是 1 了. 如果最后的写回生效的慢,你再读取 i 的值,都可能是 0,这就是缓存不一致问题.

这种情况一般称为 失效数据,因为线程1 还没将 i 的值写回主内存,所以 i 还是 0,在线程2 中读到的就是 i 的失效值(旧值).

也可以理解成,在操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的.

有序性

有序性: 即程序执行的顺序按照代码的先后顺序执行. 举个简单的例子,看下面这段代码:

int i = 0;
boolean flag = false;
i = 1;        //语句1
flag = true;     //语句2

上面代码定义了一个 int 型变量,定义了一个 boolean 类型变量,然后分别对两个变量进行赋值操作.

从代码顺序上看,语句1 是在语句2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句1 一定会在语句2 前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序.

重排序

指令重排是指 JVM 在编译 Java 代码的时候,或者 CPU 在执行 JVM 字节码的时候,对现有的指令顺序进行重新排序.

它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的(指的是不改变单线程下的程序执行结果).

虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? 再看下面一个例子:

int a = 10;  //语句1
int r = 2;  //语句2
a = a + 3;  //语句3
r = a*a;   //语句4

这段代码有 4 个语句,那么可能的一个执行顺序是:

java 并发编程之共享变量的实现方法


那么可不可能是这个执行顺序呢?

语句2 语句1 语句4 语句3.

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 Instruction 2 必须用到 Instruction 1 的结果,那么处理器会保证 Instruction 1 会在 Instruction 2 之前执行.

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢? 下面看一个例子:

//线程1:
context = loadContext();  //语句1
inited = true;       //语句2

//线程2:
while(!inited ){
 sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1 和语句2 没有数据依赖性,因此可能会被重排序.

假如发生了重排序,在线程1 执行过程中先执行语句2,而此时线程2 会以为初始化工作已经完成,那么就会跳出 while 循环,去执行 doSomethingwithconfig(context) 方法,而此时 context 并没有被初始化,就会导致程序出错.

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性.

原子性

Java 中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行.

JMM 只实现了基本的原子性,像 i++ 的操作,必须借助于 synchronizedLock 来保证整块代码的原子性了. 线程在释放锁之前,必然会把 i 的值刷回到主存的.

重点,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性. 只要有一个没有被保证,就有可能会导致程序运行不正确.

volatile 关键字

volatile 关键字的两层语义

一旦一个共享变量 (类的成员变量、类的静态成员变量) 被 volatile 修饰之后,那么就具备了两层语义:

1) 禁止进行指令重排序.

2) 读写一个变量时,都是直接操作主内存.

在一个变量被 volatile 修饰后,JVM 会为我们做两件事:

1.在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障.

2.在每个 volatile 读操作前插入 LoadLoad 屏障,在读操作后插入 LoadStore 屏障.

或许这样说有些抽象,我们看一看刚才线程A代码的例子:

boolean contextReady = false;

//在线程A中执行:
context = loadContext();
contextReady = true;

我们给 contextReady 增加 volatile 修饰符,会带来什么效果呢?

java 并发编程之共享变量的实现方法


由于加入了 StoreStore 屏障,屏障上方的普通写入语句 context = loadContext() 和屏障下方的 volatile 写入语句 contextReady = true 无法交换顺序,从而成功阻止了指令重排序.

java 并发编程之共享变量的实现方法


也就是说,当程序执行到 volatile 变量的读或写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见.

volatile特性之一:
保证变量在线程之间的可见性. 可见性的保证是基于 CPU 的内存屏障指令,被 JSR-133 抽象为 happens-before 原则.

volatile特性之二:
阻止编译时和运行时的指令重排. 编译时 JVM 编译器遵循内存屏障的约束,运行时依靠 CPU 屏障指令来阻止重排.

volatile 除了保证可见性和有序性,还解决了 long 类型和 double 类型数据的 8 字节赋值问题.
虚拟机规范中允许对 64 位数据类型,分为 2 次 32 位的操作来处理,当读取一个非 volatile 类型的 long 变量时,如果对该变量的读操作和写操作不在同一个线程中执行,那么很有可能会读取到某个值得高 32 位和另一个值得低 32 位.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 连接 连接池产生原因 连接池实现原理 小结 TEMPERANCE:Eat not to dullness;drink not to elevation.节制
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 一个优秀的工程师和一个普通的工程师的区别,不是满天飞的架构图,他的功底体现在所写的每一行代码上。-- 毕玄 1. 命名风格 【书摘】类名用 UpperCamelC
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个人在用”。哪怕只是throw了一个新的Exception。哈哈,这是我犯的错误。一、接口和抽象类类,即一个对象。先抽象类,就是抽象出类的基础部分,即抽象基类(抽象类)。官方定义让人费解,但是记忆方法是也不错的 —包含抽象方法的类叫做抽象类。接口
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket一、引子文件,作为常见的数据源。关于操作文件的字节流就是 —FileInputStream&FileOutputStream。
作者:泥沙砖瓦浆木匠网站:http://blog.csdn.net/jeffli1993个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节。交流QQ群:【编程之美 365234583】http://qm.qq.com/cgi-bin/qm/qr?k=FhFAoaWwjP29_Aonqz
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程与多线程 线程是什么? 线程(Thread)是一个对象(Object)。用来干什么?Java 线程(也称 JVM 线程)是 Java 进程内允许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。 Ja
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket在面向对象编程中,编程人员应该在意“资源”。比如?1String hello = "hello"; 在代码中,我们
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 《程序兵法:Java String 源码的排序算法(一)》 文章工程:* JDK 1.8* 工程名:algorithm-core-le
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 一、父子类变量名相同会咋样? 有个小故事,今天群里面有个人问下面如图输出什么? 我回答:60。但这是错的,答案结果是 40 。我知错能改,然后说了下父子类变
作者:泥瓦匠 出处:https://www.bysocket.com/2021-10-26/mac-create-files-from-the-root-directory.html Mac 操作系统挺适合开发者进行写代码,最近碰到了一个问题,问题是如何在 macOS 根目录创建文件夹。不同的 ma
作者:李强强上一篇,泥瓦匠基础地讲了下Java I/O : Bit Operation 位运算。这一讲,泥瓦匠带你走进Java中的进制详解。一、引子在Java世界里,99%的工作都是处理这高层。那么二进制,字节码这些会在哪里用到呢?自问自答:在跨平台的时候,就凸显神功了。比如说文件读写,数据通信,还
1 线程中断 1.1 什么是线程中断? 线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程调用了该线程的 方法,使得该线程中断标志位属性改变。 深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性,
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want需求 项目在设计表的时候,要处理并发多的一些数据,类似订单号不能重复,要保持唯一。原本以为来个时间戳,精确到毫秒应该不错了。后来觉得是错了,测试环境下很多一
纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 加微信:bysocket01
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want.文章Points:1、介绍RESTful架构风格2、Spring配置CXF3、三层初设计,实现WebService接口层4、撰写HTTPClient 客户
Writer :BYSocket(泥沙砖瓦浆木匠)什么是回调?今天傻傻地截了张图问了下,然后被陈大牛回答道“就一个回调…”。此时千万个草泥马飞奔而过(逃哈哈,看着源码,享受着这种回调在代码上的作用,真是美哉。不妨总结总结。一、什么是回调回调,回调。要先有调用,才有调用者和被调用者之间的回调。所以在百
Writer :BYSocket(泥沙砖瓦浆木匠)一、什么大小端?大小端在计算机业界,Endian表示数据在存储器中的存放顺序。百度百科如下叙述之:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加
What is a programming language? Before introducing compilation and decompilation, let's briefly introduce the Programming Language. Programming la
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket泥瓦匠喜欢Java,文章总是扯扯Java。 I/O 基础,就是二进制,也就是Bit。一、Bit与二进制什么是Bit(位)呢?位是CPU
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocket一、前言 泥瓦匠最近被项目搞的天昏地暗。发现有些要给自己一些目标,关于技术的目标:专注很重要。专注Java 基础 + H5(学习) 其他操作系统,算法,数据结构当成课外书博览。有时候,就是那样你越是专注方面越