Java多线程:易失性变量,事前关联和内存一致性

什么是volatile变量?

volatile是Java中的关键字。 您不能将其用作变量或方法名称。 期。

我们什么时候应该使用它?

哈哈,对不起,没办法。

当我们在多线程环境中与多个线程共享变量时,我们通常使用volatile关键字,并且我们希望避免由于这些变量在CPU缓存中的缓存而导致任何内存不一致错误 。

考虑下面的生产者/消费者示例,其中我们一次生产/消费一件商品:

public class ProducerConsumer {private String value = "";private boolean hasValue = false;public void produce(String value) {while (hasValue) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("Producing " + value + " as the next consumable");this.value = value;hasValue = true;}public String consume() {while (!hasValue) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}String value = this.value;hasValue = false;System.out.println("Consumed " + value);return value;}
}

在上述类中, Produce方法通过将其参数存储到value中并将hasValue标志更改为true来生成一个新值。 while循环检查值标志( hasValue )是否为true,这表示存在尚未使用的新值,如果为true,则请求当前线程进入睡眠状态。 仅当hasValue标志已更改为false时,此睡眠循环才会停止,这只有在consumer方法使用了新值的情况下才有可能。 如果没有新值可用,那么消耗方法将请求当前线程休眠。 当Produce方法产生一个新值时,它将终止其睡眠循环,使用它并清除value标志。

现在想象一下,有两个线程正在使用此类的对象–一个正在尝试产生值(写线程),另一个正在使用它们(读线程)。 以下测试说明了这种方法:

public class ProducerConsumerTest {@Testpublic void testProduceConsume() throws InterruptedException {ProducerConsumer producerConsumer = new ProducerConsumer();List<String> values = Arrays.asList("1", "2", "3", "4", "5", "6", "7", "8","9", "10", "11", "12", "13");Thread writerThread = new Thread(() -> values.stream().forEach(producerConsumer::produce));Thread readerThread = new Thread(() -> {for (int i = 0; i > values.size(); i++) {producerConsumer.consume();}});writerThread.start();readerThread.start();writerThread.join();readerThread.join();}
}

该示例在大多数情况下将产生预期的输出,但也很有可能陷入僵局!

怎么样?

让我们再谈一下计算机体系结构。

我们知道一台计算机由CPU和内存单元(以及许多其他部件)组成。 即使主存储器是我们所有程序指令和变量/数据所在的位置,在程序执行期间,CPU仍可以将变量的副本存储在其内部存储器(称为CPU缓存)中,以提高性能。 由于现代计算机现在具有不止一个CPU,因此也有不止一个CPU缓存。

在多线程环境中,可能有多个线程同时执行,每个线程都在不同的CPU中运行(尽管这完全取决于基础操作系统),并且每个线程都可以从main复制变量。内存放入相应的CPU缓存中。 当线程访问这些变量时,它们随后将访问这些缓存的副本,而不是主内存中的实际副本。

现在,假设测试中的两个线程在两个不同的CPU上运行,并且hasValue标志已缓存在其中一个(或两个)上。 现在考虑以下执行顺序:

  1. writerThread产生一个值,并将hasValue更改为true。 但是,此更新仅反映在缓存中,而不反映在主存储器中。
  2. readerThread尝试使用一个值,但是hasValue标志的缓存副本设置为false。 因此,即使writerThread产生了一个值,它也无法使用它,因为线程无法脱离睡眠循环( hasValue为false)。
  3. 由于readerThread没有使用新生成的值, writerThread不能继续进行,因为该标志没有被清除,因此它将停留在其休眠循环中。
  4. 而且我们手中有一个僵局!

仅当hasValue标志跨所有缓存同步时,这种情况才会改变,这完全取决于基础操作系统。

volatile如何适合此示例?

如果仅将hasValue标志标记为volatile ,则可以确保不会发生这种类型的死锁:

private volatile boolean hasValue = false;

将变量标记为volatile将强制每个线程直接从主内存读取该变量的值。 而且,每次对volatile变量的写操作都会立即刷新到主存储器中。 如果线程决定缓存该变量,则它将在每次读/写时与主内存同步。

进行此更改后,请考虑导致死锁的先前执行步骤:

  1. 作家线程   产生一个值,并将hasValue更改为true。 这次更新将直接反映到主内存中(即使已缓存)。
  2. 读取器线程正在尝试使用一个值,并检查hasValue的值 这次,每次读取将强制直接从主内存中获取值,因此它将获取写入线程所做的更改。
  3. 阅读器线程使用生成的值,并清除标志的值。 这个新值将进入主内存(如果已缓存,则缓存的副本也将被更新)。
  4. 编写器线程将接受此更改,因为每个读取现在都在访问主内存。 它将继续产生新的价值。

瞧! 我们都很高兴^ _ ^!

这是所有易失性行为,迫使线程直接从内存中读取/写入变量吗?

实际上,它还具有其他含义。 访问易失性变量在程序语句之间建立先发生后关系。

什么是

两个程序语句之间的先发生后关系是一种保证,可以确保一个语句写的任何内存对另一条语句可见。

它与

当我们写入一个volatile变量时,它会以后每次读取该相同变量时创建一个事前发生的关系。 因此,直到对该易失性变量进行写操作之前完成的所有内存写操作,对于该易失性变量的读取之后的所有语句,随后都将可见。

Err….Ok…。我明白了,但也许是一个很好的例子。

好的,对模糊的定义表示抱歉。 考虑以下示例:

// Definition: Some variables
private int first = 1;
private int second = 2;
private int third = 3;
private volatile boolean hasValue = false;// First Snippet: A sequence of write operations being executed by Thread 1
first = 5;
second = 6;
third = 7;
hasValue = true;// Second Snippet: A sequence of read operations being executed by Thread 2
System.out.println("Flag is set to : " + hasValue);
System.out.println("First: " + first);  // will print 5
System.out.println("Second: " + second); // will print 6
System.out.println("Third: " + third);  // will print 7

假设上面的两个代码片段由两个不同的线程(线程1和2)执行。当第一个线程更改hasValue时 ,它不仅会将此更改刷新到主内存,还将导致前三个写操作(以及其他任何写操作)先前的写入)也要刷新到主存储器中! 结果,当第二个线程访问这三个变量时,它将看到线程1进行的所有写操作,即使它们之前都已被缓存(这些缓存的副本也将被更新)!

这就是为什么我们在第一个示例中也不必用volatile标记变量的原因。 由于我们在访问hasValue之前已写入该变量,并且在读取hasValue之后已从该变量读取,因此该变量会自动与主内存同步。

这还有另一个有趣的结果。 JVM以其程序优化而闻名。 有时,它会重新排列程序语句以提高性能,而不会更改程序的输出。 例如,它可以更改以下语句序列:

first = 5;
second = 6;
third = 7;

到这个:

second = 6;
third = 7;
first = 5;

但是,当语句涉及访问volatile变量时,它将永远不会移动发生在volatile写入之后的语句。 这意味着它将永远不会改变:

first = 5;  // write before volatile write
second = 6;  // write before volatile write
third = 7;   // write before volatile write
hasValue = true;

到这个:

first = 5;
second = 6;
hasValue = true;
third = 7;  // Order changed to appear after volatile write! This will never happen!

即使从程序正确性的角度来看,它们似乎都是等效的。 请注意,只要它们都出现在易失性写入之前,仍然允许JVM重新排序其中的前三个写入。

同样,JVM也不会更改在读取易失性变量后出现在访问之前的语句的顺序。 这意味着:

System.out.println("Flag is set to : " + hasValue);  // volatile read
System.out.println("First: " + first);  // Read after volatile read
System.out.println("Second: " + second); // Read after volatile read
System.out.println("Third: " + third);  // Read after volatile read

JVM绝不会将其转换为:

System.out.println("First: " + first);  // Read before volatile read! Will never happen!
System.out.println("Fiag is set to : " + hasValue); // volatile read
System.out.println("Second: " + second); 
System.out.println("Third: " + third);

但是,JVM可以肯定它们中最后三个读取的顺序,只要它们在可变读取之后一直出现。

我认为必须为易失性变量付出性能损失。

您说对了,因为易失性变量会强制访问主内存,并且访问主内存总是比访问CPU缓存慢。 它还会阻止JVM对某些程序进行优化,从而进一步降低性能。

我们是否可以始终使用易变变量来维护线程之间的数据一致性?

不幸的是没有。 当有多个线程读写同一变量时,将其标记为volatile不足以保持一致性。 考虑以下UnsafeCounter类:

public class UnsafeCounter {private volatile int counter;public void inc() {counter++;}public void dec() {counter--;}public int get() {return counter;}
}

和以下测试:

public class UnsafeCounterTest {@Testpublic void testUnsafeCounter() throws InterruptedException {UnsafeCounter unsafeCounter = new UnsafeCounter();Thread first = new Thread(() -> {for (int i = 0; i < 5; i++) { unsafeCounter.inc();}});Thread second = new Thread(() -> {for (int i = 0; i < 5; i++) {unsafeCounter.dec();}});first.start();second.start();first.join();second.join();System.out.println("Current counter value: " + unsafeCounter.get());}
}

该代码非常不言自明。 我们在一个线程中增加计数器,而在另一个线程中减少相同次数。 运行此测试后,我们希望计数器保持0,但这不能保证。 在大多数情况下,它将为0,在某些情况下,它将为-1,-2、1、2,即[-5,5]范围内的任何整数值。

为什么会这样? 发生这种情况是因为计数器的递增和递减操作都不是原子的-它们不会一次全部发生。 它们都由多个步骤组成,并且步骤顺序相互重叠。 因此,您可以考虑以下增量操作:

  1. 读取计数器的值。
  2. 添加一个。
  3. 写回计数器的新值。

递减操作如下:

  1. 读取计数器的值。
  2. 从中减去一个。
  3. 写回计数器的新值。

现在,让我们考虑以下执行步骤:

  1. 第一个线程已从内存中读取计数器的值。 最初将其设置为零。 然后向其中添加一个。
  2. 第二个线程还从内存中读取了计数器的值,并看到将其设置为零。 然后从中减去一个。
  3. 现在,第一个线程将counter的新值写回内存,将其更改为1。
  4. 现在,第二个线程将计数器的新值写回内存,即-1。
  5. 第一线程的更新丢失。

我们如何防止这种情况?

通过使用同步:

public class SynchronizedCounter {private int counter;public synchronized void inc() {counter++;}public synchronized void dec() {counter--;}public synchronized int get() {return counter;}
}

或使用AtomicInteger :

public class AtomicCounter {private AtomicInteger atomicInteger = new AtomicInteger();public void inc() {atomicInteger.incrementAndGet();}public void dec() {atomicInteger.decrementAndGet();}public int get() {return atomicInteger.intValue();}
}

我个人的选择是使用AtomicInteger作为同步对象,因为只有一个线程可以访问任何inc / dec / get方法,从而大大降低了性能。

意思是不是……..?

对。 使用synced关键字还可以建立语句之间的事前发生关系。 输入同步的方法/块将在它之前出现的语句与该方法/块内部的语句之间建立先发生后关系。 有关建立事前关系的完整列表,请转到此处 。

就暂时而言,这就是我要说的。

  • 所有示例都已上传到我的github存储库中 。

翻译自: https://www.javacodegeeks.com/2015/11/java-multi-threading-volatile-variables-happens-before-relationship-and-memory-consistency.html

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

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

相关文章

20145219 《信息安全系统设计基础》第01周学习总结

20145219 《信息安全系统设计基础》第01周学习总结 教材学习内容总结 别出心裁的Linux命令学习法 1、Ubuntu快捷键 CTRLALTT:打开终端&#xff1b;CTRLSHIFTT&#xff1a;新建标签页&#xff1b;ALT数字N&#xff1a;终端中切换到第N个标签页&#xff1b;Tab:终端中命令补全&…

拉盖尔多项式 matlab,类氢原子的定态波函数

&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp预备知识 球坐标系中的定态薛定谔方程&#xff0c;原子单位制本文使用原子单位制&#xff0e;类氢原子(hydrogen-like atom)被定义为原子核有 $Z$ 个质子(核电荷为 $Ze$)有一个核外电子的原子/离子&#xff0…

再论EM算法的收敛性和K-Means的收敛性

标签&#xff08;空格分隔&#xff09;&#xff1a; 机器学习 &#xff08;最近被一波波的笔试面试淹没了&#xff0c;但是在有两次面试时被问到了同一个问题&#xff1a;K-Means算法的收敛性。在网上查阅了很多资料&#xff0c;并没有看到很清晰的解释&#xff0c;所以希望可以…

java xml dom getelementbyid,DOM中常见的元素获取方式

1.getElementById获取元素 返回的是一个元素对象var timer document.getElementById(time);console.dir 打印返回元素对象&#xff0c;更好的查看里面的属性和方法console.dir( timer );2.getElementsByTagName 获取某类标签元素 返回的是 获取过来元素对象的集合 以…

杰尔·地狱

什么是JAR地狱&#xff1f; &#xff08;或者是classpath地狱&#xff1f;还是依赖地狱&#xff1f;&#xff09;在考虑使用Maven或OSGi等现代开发工具时&#xff0c;哪些方面仍然有意义&#xff1f; 有趣的是&#xff0c;似乎没有对这些问题的结构化答案&#xff08;即&#…

matlab radsimp,[转载]MATLAB学习笔记(八)

符号运算符号运算分为以下几类&#xff1a;符号表达式和符号矩阵的操作整体定义为符号微积分符号线性方程符号微分方程A、符号变量、符号表达式和符号方程的分解一、 生成符号变量要使用sym和syms&#xff1a;使用sym函数可以定义符号表达式&#xff0c;此时有两种定义方法&…

windows,python3.x下安装pyspider

由于是初学者&#xff0c;业余学习&#xff0c;习惯使用windows&#xff0c;初次了解到pyspider写代码和调试代码非常简便&#xff1b;作者binux是在Ubuntu下部署测试的。在作者的博客看到windows下安装的讨论。windows直接安装失败主要是lxml、pycurl安装失败&#xff0c;需要…

matlab实验符号计算答案,实验五matlab符号计算

实验五matlab符号计算 实验 5 符号计算 教师评分班级 学号 姓名实验日期 2014 年 6 月 17 日 星期 二 第 1 至 2 节课实验地点实验目的1. 掌握定义符号对象的办法2. 掌握符号表达式的运算法则以及符号矩阵运算3. 掌握求符号函数极限及导数的方法4. 掌握求符号函数定积分和不定积…

junit4 单元测试框架_超越JUnit –测试框架的替代方案

junit4 单元测试框架JUnit是事实上的Java单元测试框架&#xff0c;但是可能有一些新的&#xff08;不是那么新的&#xff09;框架可以用于Web开发。 在采用之前可能要问自己的问题&#xff1a; 它们是否快速&#xff0c;容易开发并因此成本低廉&#xff1f; 他们运行快并因此鼓…

Java学习笔记之:Java String类

一、引言 字符串广泛应用在Java编程中&#xff0c;在Java中字符串属于对象&#xff0c;Java提供了String类来创建和操作字符串。 创建字符串最简单的方式如下: String str "Hello world!"; String类型是特殊的引用类型&#xff0c;我们也可以通过实例化的方式来创建 …

java循环输入直到,使用循环接受其他用户输入,直到用户输入结束输入的值

我是Java的新手 . 我需要一些帮助&#xff0c;使用循环接受其他用户输入&#xff0c;直到用户输入结束输入的值 . 我的问题从语句“System.out.println(”你完成了吗&#xff1f;输入大写的Y / N)开始 . 下面是我的代码 .公共类EmployeeData {//declare variablesprivate Strin…

WildFly 10 CR 2发布– Java EE 7,Java 8,Hibernate 5,JavaScript支持热重载

昨天&#xff0c;WildFly团队发布了最新版本的WildFly 10 。 CR2很可能是预计于十月份发布最终版本之前的最后一个版本。 即使主要支持的Java EE规范是7&#xff0c;WildFly 8和WildFly 9仍具有许多新功能&#xff0c;该版本现在具有三个服务器版本&#xff0c;实现了Java EE 7…

php中什么时候用传值,php中传值与传引用的区别。什么时候传值什么时候传引用?...

java中的this与super的区别java中的this与super的区别 1. 子类的构造函数如果要引用super的话,必须把super放在函数的首位 代码如下: class Base { Base() { System.out.pr ...php传值和传引用的区别php传值和传引用的区别所谓值传递,就是说仅将对象的值传递给目标对象,就相当于…

openshift 部署_OpenShift Express:部署Java EE应用程序(支持AS7)

openshift 部署在过去的几年中&#xff0c;我越来越多地听说过“云”服务。 最初&#xff0c;我并不是很想尝试一下。 但是几个月后&#xff08;一年&#xff1f;&#xff09;&#xff0c;我决定看看这是怎么回事。 我从事Java EE开发已有7年以上&#xff0c;因此&#xff0c;我…

迪克逊准则matlab,浙江科技学院学报

引言传统的农业栽培需要遵循季节性及周期性的栽培规律,受外界环境的影响较大,相应的栽培技术无法得到良好的栽培结果[1]。而温室大棚通过环境控制器调节温室环境,为作物提供了合适的生长环境。因此,它在农业领域的应用越来越广泛,温室环境控制系统的研究成为人们关注的热点[2-5…

Linq表达式和Lambda表达式用法对比

什么是Linq表达式&#xff1f;什么是Lambda表达式&#xff1f;前一段时间用到这个只是&#xff0c;在网上也没找到比较简单明了的方法&#xff0c;今天就整理了一下相关知识&#xff0c;有空了再仔细研究研究 public Program() { List<Student> allStudent new List<…

使用FlexDeploy对融合中间件应用程序进行自动化软件测试

自动化软件测试是任何软件组织都应执行的强制性活动之一&#xff0c;以保证其产品质量。 但是&#xff0c;此过程通常变得非常复杂&#xff0c;尤其是涉及由多个不同部分组成的现代复杂系统的自动化测试时。 所有这些部分都基于不同的技术&#xff0c;显然&#xff0c;应该使用…

uploadify插件的使用

插件&#xff1a; uploadify.css jquery.uploadify.js bootstrap html代码&#xff1a; <input type"file" name"uploadify_coverimg" id"uploadify" /> <div id"the_coverimg"></div> js代码&#xff1a; $("…

wxlogin php,wxlogin.php

if($_GET[echostr]){echo $_GET[echostr];die();}/*** 开发规范&#xff1a;* 1.不同版本不同控制器以及模板* 2.不同版本不同数据库&#xff0c;但是对应数据表表结构必须一致* 3.不同版本共用service层&#xff0c;所以修改表结构必须所有版本统一*/// ---------------------…

关于人生倒计时的一个小玩意,纯属业余

人生倒计时 人生倒计时 出生年份&#xff1a; 出生月份&#xff1a; <!DOCTYPE html> <html id"spLianghui"> <head><meta http-equiv"Content-Type" content"text/html; charsetgb2312" /><title>人生倒计时<…