java多线程02-----------------volatile内存语义
volatile关键字是java虚拟机提供的最轻量级额的同步机制。由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过)。
1. java内存模型(JMM)
JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存的数据不一致、编译器处理器的指令重排序造成的各种线程安全问题,以保障多线程编程的原子性、可见性和有序性。
JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程中的工作内存中存储了该线程用到的变量的主内存的拷贝,各线程对变量的所有操作都必须在工作内存中进行,
线程之间的变量值的传递都必须通过主内存来进行。
JMM定义了8中操作实现主内存与工作内存的交互协议:
1)lock:作用于主内存,它把一个变量标识为一条线程的独占状态。
2)unlock:作用于主内存,它把一个处于锁定状态的变量的释放出来。
3)read:作用于主内存,它把一个变量的值从主内存传输到线程的工作内存中。
4)load:作用于工作内存,它把从主内存中read到的值放入工作内存的变量副本中。
5)use:作用于工作内存,它把一个变量的值从主内存传递给执行引擎
6)assign:作用与工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量。
7)store:作用于工作内存,把工作内存中一个变量的值传送到主内存。
8)write:作用于主内存,它把store操作从工作内存中得到的值放入主内存中的变量中。
这8中操作以及对着8中操作的规则的限制就能确定哪些内存访问在并发条件下是线程安全的,这种方式比较繁琐,jdk1.5之后提出了提出了happens-before规则来判断线程是否安全。
可以这么理解,happens-before规则是JMM的核心.Happens-before就是用来确定两个操作的执行顺序。这两个操作可在同一线程中,也可以在两个线程中。
happens-before规定:如果一个操作happens-before另个一操作,那么第一个操作的结果对第二个操作可见(但这并不意味着处理器必须按照happens-before顺序执行,只要不改变执行结果,可任意优化)。happens-before规则已在前边博文中介绍,这里不再重复(http://www.cnblogs.com/gdy1993/p/9117331.html)
JMM内存规则仅仅是一种规则,规则的最终落实是通过java虚拟机、编译器以及处理器一同协作来落实的,而内存屏障是java虚拟机、编译器、处理器之间沟通的纽带。
而java原因封装了这些底层的具体实现与控制,提供了synchronized、lock和volatile等关键字的来保障多线程安全问题。
2. volatile关键字
(1)volatile对可见性的保证
在介绍volatile关键字之前,先来看这样一段代码:
//线程1
boolean stop = false;
while(!stop) {
doSomething();
} //线程2
stop = true;
有两个线程:线程1和线程2,线程1在stop==false时,不停的执行doSomething()方法;线程2在执行到一定情况时,将stop设置为true,将线程1中断,很多人采用这种方式中断线程,但这并不是安全的。因为stop作为一个普通变量,线程2对其的修改,并不能立刻被线程1所感知,即线程1对stop的修改仅仅在自己的工作内存中,还没来的急写入主内存,线程2工作内存中的stop并未修改,可能导致线程无法中断,虽然这种可能性很小,但一旦发生,后果严重。
而使用volatile变量修饰就能避免这个问题,这也是volatile第一个重要含义:
volatile修饰的变量,能够保证不同线程对这个变量操作的可见性,即一个线程修改了这个变量的值,这个新值对于其他线程是立即可见的。
volatile的对可见性保证的原理:
对于volatile修饰的变量,当某个线程对其进行修改时,会强制将该值刷新到主内存,这就使得其他线程对该变量在各自工作内存中的缓存无效,因而在其他线程对该变量进行操作时,必须从主内存中重新加载
(2)volatile对原子性的保障?
首先来看这样一段代码(深入理解java虚拟机):
public class VolatileTest {
public static volatile int race = 0; public static void increase() {
race++;
} public static final int THREAD_COUNT = 20; public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for (Thread t : threads) {
t = new Thread(new Runnable() { @Override
public void run() {
for(int i = 0; i < 10000; i++) {
increase();
}
}
});
t.start();
} while(Thread.activeCount() > 1) {
Thread.yield();
} System.out.println(race);//race < 200000 }
}
race是volatile修饰的共享变量,创建20个线程对这个共享变量进行自增操作,每个线程自增的次数为10000次,如果volatile能够保证原子性的话,最终race的结果肯定是200000。但结果不然,每次程序运行race'的值总是小于200000,这也侧面证明了volatile并不能保证共享变量操作的原子性。原理如下:
线程1读取了race的值,然后cp分配的时间片结束,线程2此时读取了共享变量的值,并对race进行自增操作,并将操作后的值刷新到主内存,此时线程1已经读取了race的值,因此保留的依然是原来的值,此时这个值已是旧值,对race进行自增操作后刷新到主内存,因此主内存中的值也是旧值。这也是volatile仅仅能保障读到的是相对新值的原因。
(3)volatile对有序性的保障
首先来看这样一段代码:
//线程1
boolean initialized = false;
context = loadContext();
initialized = true; //线程2
while(!initialized) {
sleep();
} doSomething(context);
线程2在initialized变量为true时,使用context变量完成一些操作;线程1负责加载context,并在加载完成后将initialized变量设为true。但是,由于initialized只是一个普通变量,普通变量仅仅能够保证在该方法的执行过程中,所有依赖赋值结果的地方都能获得正确的值,而不能保证变量的赋值顺序与程序代码的执行顺序一致。因此就可能出现这样一种情况,当线程1将initialized变量设为true时,context依然没有加载完成,但线程2由于读到initialized为true,就可能执行了doSomething()方法,可能会产生非常奇怪的效果。
而volatile的第二个语义就是禁止重排序:
写volatile变量的操作与该操作之前的任何读写操作都不会被重排序;
读volatile变量操作与该操作之后的任何读写操作都不会重排序。
(4) volatile的底层实现原理
java语言底层是通过内存屏障来实现volatile语义的。
对于volatile变量的写操作:
①java虚拟机会在该操作之前插入一个释放屏障(loadstore+storestore),释放屏障禁止了volatile变量的写操作与该操作之前的任何读写操作的重排序。
②java虚拟机会在该操作之后插入一个存储屏障(storeload),存储屏障使得对volatile变量的写操作能够同步到主内存。
对于volatile变量的读操作:
③java虚拟机会在该操作之前插入一个loadload,使得每次对volatile变量的读取都从主内存中重新加载(刷新处理器缓存)
④java虚拟机会在该操作之后插入一个获得屏障(loadstore+loadload),使得volatile后的任何读写操作与该操作进行重排序。
①③保障可见性,②④保障有序性。
(5)volatile关键字与happens-before的关系
Happens-before规则中的volatile规则为:对于一个volatile域的写happens-before后续每一个针对该变量的读操作。
写线程执行write(),然后读线程执行read()方法,图中每个箭头都代表一个happens-before关系,黑色箭头是根据程序顺序规则,蓝色箭头根据volatile规则,红色箭头是根据传递性推出的,即操作2happens-before操作3,即对volatile共享变量的更新操作排在后续读取操作之前,对volatile变量的修改对后续volatile变量的读取可见。