一、保持多线程环境中共享变量的可见性。
在多线程环境下,每个线程都有一个自己的线程栈,对于共享的变量(比如堆中new的变量),每个线程栈都会拷贝一个该变量的副本,使用volatile修饰的变量,当线程修改当前栈程栈的副本时,修改后的副本数据会马上更新到共享变量上。
如下图所示,我们把变量data用volatile修饰,然后启动两个线程分别对data进行自增操作,在初始化的步骤中,线程1和线程2分别拷贝一份变量的副本到两个线程的栈中,两个线程内部分别进行循环,每循环一次data就自增一次。而因为data被volatile关键字修饰,则每自增一次,线程栈中的data变量副本都会强制更新到共享变量data中。同时,其它线程中的data副本将被置为无效,其它线程在使用data时会从新从堆中读取。
下面的代码,就是图中线程执行函数的例子:
public class MyRunnable implements Runnable {
public static Integer data = 0;
private Integer spon = 0;
@Override
public void run() {
try {
while (true) {
data++;
Thread.sleep(1000 * (spon / 2 + 1));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
需要注意的是:
(1)volatile关键字可以强制程序将线程栈中的副本刷新的共享变量,但不是说:如果一个共享变量没有被voltile修饰,它的副本在线程中被修改时就不会马上更新到共享变量中。实际上这种情况很少会发生。
(2)因为int等基本类型在深拷贝时才具有原子性,volatile关键字一般只用来修饰基本的数据类型。
(3)volatile关键字并不能代替synchronized等锁进行线程间的同步作用,比如在上述的代码中,data的自增操作不具有原子性,它实际分为两个过程,一是告诉缓存的数据进行自增操作,二是自增后的数据更新到内存。
二、禁止编译器的指令重排功能
在默认情况下,java编译器具有指令重拍功能,比如以下代码:
int a=1;
int b=2;
int c=a+b;
编译器在进行编译时,可能a=1在前面,也可能b=2在前面。但是并不是所有的代码都会被编译器指令重拍,比如下面的代码:
int a=1;
a=a+1;
system.out.print(a);
这种情况下,第2行的代码直接影响到了第3行的打印结果,编译器肯定不会进行指令重排。
在多线程的条件下,指令重拍可能会影响到执行执行的结果,比如下面的代码:
共享变量:
boolean flag=false;
int data=1;
线程1:
if(flag=-true){
system.out.println(data);
}
线程2:
data++;
flag=true;
显然线程2的两条指令如果重排将会直接影响线程1的输出结果,此时我们可以用volatile关键字修饰上述两个变量,这样禁止了编译器的指令重排。volatile在禁止指令重拍方面应用最广的应该算是在单例模式上,如下代码:
public class MyObj {
private static volatile MyObj instance = new MyObj();
public static MyObj getInstance() {
return instance;
}
}
在执行MyObj instance = new MyObj();时实际上被分成了三个步骤:
- 1、在栈中创建一片内存给instance变量。
- 2、在堆中new一个MyObj对象。
- 3、将步骤2中的对象指向步骤1中的变量。
在多线程环境下,如果编译器进行了指令重排,将步骤2和步骤3对调,那么我们在使用MyObj的单例对象时很可能会出现null指针异常。所以最安全的方法就是将instance对象用volatile关键字进行修饰,禁止编译器指令重排。