您的位置:首页 > 编程语言 > Java开发

不惑JAVA之JAVA基础 - volatile

2016-05-08 13:54 357 查看
volatile在多线程并发中用途非常广,原因是它有两个特性:

保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

禁止进行指令重排序。

内存模型的相关概念

要想弄明白volatile的原理,先需要知道内存模型的一些概念。

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

count = count + 1;


当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

通过在总线加LOCK#锁的方式

通过缓存一致性协议

这2种方式都是硬件层面上提供的方式。

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

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



并发编程中的三个概念

原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行;

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

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

对于有序性,这里就涉及到一个知识点指令重排序

指令重排序

什么是指令重排序呢?简单的说:假设代码有两条语句,代码顺序是语句1先于语句2执行;那么只要语句2不依赖于语句1的结果,打乱它们的顺序对最终的结果没有影响的话,那么真正交给CPU去执行时,他们的顺序可以是没有限制的。

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


简单的理解就是,可以允许语句2先于语句1被CPU执行,和代码中的顺序不一致。有的时候我们也需要防止指令重排序,所有就有了Happens-before原则。

Happens-before原则

先来看个多线程代码:

public class Test1 {
private int a=1, b=2;
public void foo(){ // 线程1
a=3;
b=4;
}

public int getA(){ // 线程2
return a;
}
public int getB(){ // 线程2
return b;
}
}


上面的代码,当线程1执行foo方法的时候,线程2访问getA和getB会得到什么样的结果?

答案:

A:a=1, b=2 // 都未改变

B:a=3, b=4 // 都改变了

C:a=3, b=2 // a改变了,b未改变

D:a=1, b=4 // b改变了,a未改变

上面的A,B,C都好理解,但是D可能会出乎一些人的预料。

这是一个多线程之间内存可见性(Visibility)顺序不一致的问题。有两种可能会造成上面的D选项。

Java编译器的重排序(Reording)操作有可能导致执行顺序和代码顺序不一致;

从线程工作内存写回主存时顺序无法保证。

JMM中一个重要问题就是:如何让多线程之间,对象的状态对于各线程的“可视性”是顺序一致的。它的解决方式就是 Happens-before 规则:

JMM为所有程序内部动作定义了一个偏序关系,叫做happens-before。要想保证执行动作B的线程看到动作A的结果(无论A和B是否发生在同一个线程中),A和B之间就必须满足happens-before关系。

我们现在来看一下“Happens-before”规则都有哪些(摘自《Java并发编程实践》):

① 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。

② 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。

③ volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。

④ 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。

⑤ 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。

⑥ 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。

⑦ 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。

⑧ 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

在Java5之前,JMM对Volatile的定义是:保证读写volatile都直接发生在main memory中,线程的working memory不进行缓存。它只承诺了读和写过程的可见性,并没有对Reording做限制,所以旧的Volatile并不太可靠。在Java5之后,JMM对volatile的语义进行了增强。就是我们看到的③ volatile变量法则。

那对于“原子化操作”怎么理解呢?看下面例子:

private static volatile int nextSerialNum = 0;

public static int generateSerialNumber(){
return nextSerialNum++;
}


上面代码中对nextSerialNum使用了volatile来修饰,根据前面“Happens-Before”法则的第三条Volatile变量法则,看似不同线程都会得到一个新的serialNumber

问题出在了 nextSerialNum++ 这条语句上,它不是一个原子化的,实际上是read-modify-write三项操作,这就有可能使得在线程1在write之前,线程2也访问到了nextSerialNum,造成了线程1和线程2得到一样的serialNumber。

所以,在使用Volatile时,需要注意

不需要互斥;

对象状态的改变是不是原子化的。

volatile的原理和实现机制



下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的。

  下面这段话摘自《深入理解Java虚拟机》:

“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

它会强制将对缓存的修改操作立即写入主存;

如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile的应用场景

有兴趣的可以看一下Java 理论与实践: 正确使用 Volatile 变量,这里就不多介绍了。

后期在介绍ConcurrentHashMap时,会了解到volatile在里面的作用。

参考:

Java并发编程:volatile关键字解析

java中volatile关键字的含义

深入理解Java内存模型(四)——volatile

聊聊并发(一)深入分析Volatile的实现原理

ConcurrentHashMap
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: