可见性

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

Java 虚拟机规范试图定义一种 Java 内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台上都能达到一致的内存访问效果。简单来说,由于 CPU 执行指令的速度是很快的,但是内存访问的速度就慢了很多,相差的不是一个数量级,所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存。

在 Java 内存模型里,对上述的优化又进行了一波抽象。JMM 规定所有变量都是存在主存中的,类似于上面提到的普通内存,每个线程又包含自己的工作内存,方便理解就可以看成 CPU 上的寄存器或者高速缓存。所以线程的操作都是以工作内存为主,它们只能访问自己的工作内存,且工作前后都要把值在同步回主内存。简单点说就是:多线程中读取或修改共享变量时,首先会读取这个变量到自己的工作内存中成为一个副本,对这个副本进行改动后再更新回主内存中。

使用工作内存和主存虽然加快了速度,但是也带来了一些问题,比如:

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个语句,那么可能的一个执行顺序是:

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

语句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++ 的操作,必须借助于 synchronized 和 Lock 来保证整块代码的原子性了。线程在释放锁之前,必然会把 i 的值刷回到主存的。

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

volatile 关键字

volatile 关键字的两层语义

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

禁止进行指令重排序。

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

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

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

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

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

boolean contextReady = false;

// 在线程A中执行:

context = loadContext();

contextReady = true;

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

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

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

volatile特性之一:

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

volatile特性之二:

阻止编译时和运行时的指令重排。编译时 JVM 编译器遵循内存屏障的约束,运行时依靠 CPU 屏障指令来阻止重排。

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

java 变量共享_Java并发编程之共享变量相关推荐

  1. java计算时间差_JAVA并发编程三大Bug源头(可见性、原子性、有序性),彻底弄懂...

    原创声明:本文转载自公众号[胖滚猪学编程]​ 某日,胖滚猪写的代码导致了一个生产bug,奋战到凌晨三点依旧没有解决问题.胖滚熊一看,只用了一个volatile就解决了.并告知胖滚猪,这是并发编程导致的 ...

  2. java 变量锁_并发编程高频面试题:可重入锁+线程池+内存模型等(含答案)

    对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一.因为并发编程是Java语言中最为晦涩的知识点,它涉及操作系统.内存.CPU.编程语言等多方面的基础能力,更为考验一个程序 ...

  3. java投票锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...

  4. java volatile 原子性_Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  5. java内存 海子_Java并发编程:从根源上解析volatile关键字的实现

    Java并发编程:volatile关键字解析 1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析volatile关键字 使用volatile关键字的场景 2.内存模型的 ...

  6. java cas机制_java并发编程中的CAS机制,你理解嘛?

    学习Java并发编程,CAS机制都是一个不得不掌握的知识点.这篇文章主要是从出现的原因再到原理进行一个解析.希望对你有所帮助. 一.为什么需要CAS机制? 为什么需要CAS机制呢?我们先从一个错误现象 ...

  7. java lock 对象_Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  8. java 延迟初始化_Java并发编程——延迟初始化占位类模式

    --仅作笔记使用,内容多摘自<java并发编程实战> 在并发编程中,如果状态变量仅在单个线程中初始化和使用,自然是线程安全的,但一旦涉及到线程间的数据交互,如何声明一个用于多线程的单例状态 ...

  9. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

最新文章

  1. ICMP重定向(ICMP Redirect)
  2. How Tomcat Works(十一)
  3. 絮语----工作四年的碎碎念
  4. 可视化数据库管理工具DataGrip使用详解
  5. HashCode和equal方法
  6. 巧妙解决element-ui下拉框选项过多的问题
  7. 改变support中AlertDialog的样式
  8. 张孝祖的第一次作业展示
  9. 【更新】火星人敏捷开发手册 2011-12-31
  10. 【DS】atoi()实现
  11. [禅悟人生]先将小事做好再来修禅心
  12. python与c语言数据交互,python与c语言交互---学习012
  13. js实现图片放大镜效果——简单方法
  14. 树莓派开机自动运行python程序的两种方式
  15. 浅谈文字编码和Unicode(上)
  16. 什么是架构师?Java架构师一般多少年薪呢?
  17. P1606 [USACO07FEB]白银莲花池Lilypad Pond
  18. 线性代数之特征值与特征向量的求法
  19. 脸部表情,走路姿势,微表情
  20. esxtop 指标%RDY,NUMA,Wide-VMs

热门文章

  1. 《打破思维的墙》读后感
  2. OKR:打破组织中的沟通壁垒
  3. NR SSB概述 - PSS/SSS序列及PBCH
  4. 百度地图API——多点路径连线问题
  5. mysql如何盈利_mysql到底是不是免费的?
  6. 口水了,各大互联网大厂年终奖一览表!
  7. 论文查重一般包括哪些部分呢?
  8. 3天25顿的潮汕美食记
  9. Qt/C++ 数据库SQL 增删改查 语句示例
  10. linux系统如何扩展屏幕,大神教你用 autoplank 在多个显示器上使用 Plank 扩展坞