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方法使用了新值时才有可能。 如果没有新值可用,那么消耗方法将请求当前线程Hibernate。 当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不能继续进行,因为该标志没有被清除,因此它将停留在其Hibernate循环中。
  4. 而且我们手中有一个僵局!

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

volatile如何适合此示例?

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

private volatile boolean hasValue = false;

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

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

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

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

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

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

什么是

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

它与

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

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

java 多线程变量可见性

java 多线程变量可见性_Java多线程:易变变量,事前关联和内存一致性相关推荐

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

    什么是volatile变量? volatile是Java中的关键字. 您不能将其用作变量或方法名称. 期. 我们什么时候应该使用它? 哈哈,对不起,没办法. 当我们在多线程环境中与多个线程共享变量时, ...

  2. java多线程死锁代码_java多线程死锁 编写高质量代码:改善Java程序的151个建议...

    java多线程死锁 编写高质量代码:改善Java程序的151个建议 第1章 Java开发中的通用方法和准则 建议1:不要在常量和变量中出现易混淆的字母 建议2:莫让常量蜕变成变量 建议3:三元操作符的 ...

  3. java 易变变量_关于java:易变变量和其他变量

    以下是经典Concurency in Practice的内容: When thread A writes to a volatile variable and subsequently thread ...

  4. java线程代码实现_Java 多线程代码实现讲解

    作为一个完全面向对象的语言,Java提供了类 java.lang.Thread 来方便多线程编程,这个类提供了大量的方法来方便我们控制自己的各个线程.那么如何提供给 Java 我们要线程执行的代码呢? ...

  5. java 锁旗标_Java多线程

    Java多线程 1. 多线程存在的意义 多线程最大的作用就是能够创建"灵活响应"的桌面程序,而编写多线程最大的困难就是不同线程之间共享资源的问题,要使这些资源不会同时被多个线程访问 ...

  6. java线程怎么用_Java多线程基本使用

    一.概念 1.进程 1.1进程:是一个正在进行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元. 1.2线程:就是进程中一个独立的控制单元,线程在控制着进程的执行,一 ...

  7. java如何配置环境_java如何配置环境变量

    展开全部 首先下载好jdk,直接双击就可以安装,安装比较简单,基本都是点62616964757a686964616fe78988e69d8331333365653832击"下一步" ...

  8. Java入参关键字_Java基础17-成员变量、return关键字和多参方法

    1.成员变量 在类中声明的变量为成员变量 //Dog类 class Dog{ String name;//成员变量 } public class Test1{ public static void m ...

  9. java用一个方法对变量初始化_java中怎么给变量初始化?

    展开全部 不同的变量初始化32313133353236313431303231363533e4b893e5b19e31333337613764方法不同. 变量包括:类的属性,或者叫值域 方法里的局部变 ...

最新文章

  1. 查询两张表 然后把数据并在一起_工作表数据查询时,类似筛选功能LIKE和NOT LIKE的应用...
  2. $@ 与 $* 差在哪?
  3. 皮一皮:可怜的西瓜...
  4. Adobe Premiere Pro CC 2018下载安装方法讲解
  5. Linux查看swap使用情况小脚本
  6. 牛式 Prime Cryptarithm
  7. r语言清除变量_R语言(1)初识与数据结构
  8. 从电子工程师到研发经理到老板的多面人生
  9. spring mvc学习(23):eclipse创建Maven项目没有src/main/java并不能新建的问题
  10. Spring系列(六) Spring Web MVC 应用构建分析
  11. 2.4_double-ended_queue_双向队列
  12. php单引号中变量,php中单引号双引号那点事---顺便说说把php变量的值传给js
  13. 关于sublime出现PyV8binary错误
  14. acwing1282. 搜索关键词(AC 自动机)
  15. 函数求和公式计算机出库入库,Excel 库存统计相关函数及制作库存统计表
  16. PDF文件怎样修改,怎么修改PDF文件内容
  17. Mac用自带软件QuickTime Player进行录屏
  18. 省-市-区三级联动选择地址 + 地图定位(高德api定位获取位置信息),互相联动显示
  19. BZOJ2818 Gcd
  20. 关于TensorFlow使用GPU加速

热门文章

  1. P2387-[NOI2014]魔法森林【LCT】
  2. 【2018.3.17】模拟赛之四-ssl1864jzoj1368 燃烧木棒【最短路,Floyd】
  3. 2020 ICPC亚洲区域赛(沈阳)F-Kobolds and Catacombs(思维+模拟)
  4. 【bfs】重力球(luogu 7473/NOI Online 2021 普及组 T3)
  5. 【结论】棋盘(jzoj 2297)
  6. MySQL主从数据库配置和常见问题
  7. Java中关于String类型的10个问题
  8. Oracle入门(十三A2)之单行函数
  9. Oracle入门(五F)之11g show spparameter 命令的使用
  10. 《朝花夕拾》金句摘抄(二)