并发

Java并发编程:JMM(Java内存模型)和volatile

1.并发编程的3个概念并发编程时,要想并发程序正确地执行,必须要保证原子性、可见性和有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。1.1.原子性原子性:即一个或多个操作要么全部执行并且执行过程中不会被打断,要么都不执行。一个经典的例子就是银行转账:从账户A向账户B转账1000元,此时包含两个操作:账户A减去1000元,账户B加上1000元。这两个操作必须具备原子性才能保证转账安全。假

1. 并发编程的3个概念

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

1.1. 原子性

原子性:即一个或多个操作要么全部执行并且执行过程中不会被打断,要么都不执行。

一个经典的例子就是银行转账:从账户A向账户B转账1000元,此时包含两个操作:账户A减去1000元,账户B加上1000元。这两个操作必须具备原子性才能保证转账安全。假如账户A减去1000元之后,操作被打断了,账户B却没有收到转过来的1000元,此时就出问题了。 

 

1.2. 可见性

可见性:即多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的最新值。

例如下段代码,线程1修改i的值,线程2却没有立即看到线程1修改的i的最新值:

复制代码

//线程1执行的代码int i = 0;
i = 10; 
//线程2执行的代码j = i;

复制代码

假如执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i=10 时,会将CPU1的高速缓存中i的值赋值为10,却没有立即写入主内存中。此时线程2执行 j=i,会先从主内存中读取i的值并加载到CPU2的高速缓存中,此时主内存中的i=0,那么就会使得j最终赋值为0,而不是10。

 

1.3. 有序性

有序性:即程序执行的顺序按代码的先后顺序执行。

例如下面这段代码:

int i = 0;boolean flag = false;
i = 1;
flag = true;

在代码顺序上 i=1 在 flag=true 前面,而 JVM 在真正执行代码的时候不一定能保证 i=1 在flag=true 前面执行,这里就发生了指令重排序。

 

指令重排序

一般是为了提升程序运行效率,编译器或处理器通常会做指令重排序:

  • 编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

  • 处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。CPU 在指令重排序时会考虑指令之间的数据依赖性,如果指令2必须依赖用到指令1的结果,那么CPU会保证指令1在指令2之前执行。

指令重排序不保证程序中各个语句的执行顺序和代码中的一致,但会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上例中的代码, i=1 和 flag=true 两个语句先后执行对最终的程序结果没有影响,就有可能 CPU 先执行 flag=true,后执行 i=1。

2. java 内存模型

由于 volatile 关键字是与 java 内存模型相关的,因此了解 volatile 前,需要先了解下 java 内存模型相关概念 

2.1. 硬件效率与缓存一致性

计算机执行程序时,每条指令都是在 CPU 中执行的,而执行指令过程中,势必涉及到数据的读取和写入。CPU 在与内存交互时,需要读取运算数据、存储结果数据,这些 I/O 操作的速度与 CPU 的处理速度有几个数量级的差距,所以不得不加入一层读写速度尽可能接近 CPU 运算速度的高速缓存(Cache)来作为内存与 CPU 之间的缓冲:将运算需要使用的数据复制到高速Cache中;运算结束后再从高速Cache同步回内存中。这样 CPU 就无需等待缓慢的内存读写了。

这在单线程中运行是没有问题的,但在多线程中运行就引入了 缓存一致性 的问题:在多处理系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个 CPU 的运算任务都涉及同一主内存区域时,将可能导致各自的缓存数据不一致,此时同步回主内存时以谁的数据为准呢?

为了解决缓存一致性问题,通常有两种解决方法:

  1. 在总线加 LOCK# 锁的方式

  2. 缓存一致性协议

早期的 CPU 中,通过在总线上加 LOCK# 锁的形式来解决,因为 CPU 在和其他部件通信时都是通过总线进行,如果对总线加 LOCK# 锁,也就阻塞了 CPU 对其他部件访问(如内存),而使得只能有一个 CPU 使用这个变量的内存。

但这种方式有一个问题,在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。

所有就出现了缓存一致性协议,最著名的就是 Intel 的 MESI 协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的, 它的核心思想是:CPU写数据时,如果操作的变量是共享变量(其他 CPU 的高速缓存中也存在该变量的副本),会发出信号通知其他 CPU 将该变量的缓存设置为无效状态,那么当其他 CPU 读取该变量时,就会从内存重新读取。

JVM 有自己的内存模型,在访问缓存时,遵循一些协议来解决缓存一致性的问题。

 

2.2. 主内存和工作内存

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

Java 内存模型主要目标:是定义程序中各个变量的访问规则,即存储变量到内存和从内存中取出变量这样的底层细节。为了较好的执行性能,Java 内存模型并没有限制使用 CPU 的寄存器和高速缓存来提升指令执行速度,也没有限制编译器对指令做重排序。也就是说:在 Java 内存模型中,也会存在缓存一致性问题和指令重排序问题。

Java 内存模型规定所有的变量(包括实例字段、静态字段、构成数组对象的元素,不包括线程私有的局部变量和方法参数,因为这些不会出现竞争问题)都存储在主内存中,每条线程有自己的工作内存(可与之前将讲的CPU高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存拷贝副本。线程对变量的所有操作(read,write)都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递需要通过主内存来完成。如图所示: 

https://img1.mukewang.com/5addf224000163ca27561266.jpg

 

2.3. JMM如何处理原子性

像以下语句:

x = 10;     //语句1y = x;      //语句2x++;        //语句3x = x + 1;  //语句4

只有语句1才是原子性的操作,其他都不是原子性操作。 

语句1是直接将10赋值给x变量,也就是说线程执行这个语句时,会直接将10写入到工作内存中。 

语句2包含了两个操作,先读取x的值,然后将x的值写入到工作内存赋值给y,这两个操作合起来就不是原子性操作了。 

语句3和4都包括3个操作,先读取x的值,然后加1操作,最后写入新值。

单线程环境下,我们可以认为整个步骤都是原子性的。但多线程环境下则不同,只有基本数据类型的访问读写是具备原子性的,如果还需要提供更大范围的原子性保证,可以使用同步代码块 -- synchronized

原创不易,完成人机校验,阅读全文

相关推荐