为什么用volatile
多线程开发有三个特性
- 原子性
- 可见性
- 有序性
原子性(Atomicity)
一个操作中cpu不可以中途暂停然后再调度,要不就执行完成,要么不执行.java提供了很多机制来保证原子性.
如a++就不满足原子性,可能运行的时候做了一半就不做了。
为了解决这个问题,java提供了很多类,比如AtomicInteger,AtomicLong,AtomicReference等
可见性(visibility)
当一个线程修改了线程共享变量的值,其它线程能够立刻得知这个修改.
每个线程都有一份自己的本地内存,所有线程公用一份主内存,如果一个线程对主内存的数据进行了修改,而此时另一个线程不知道是否已经发生了修改,就说此时是不可见的.
这种不可见的状况会带来一个问题,两个线程有可能操作同一份但是值不一样的数据,于是volatile关键字就起作用了.
volatile关键字的作用:一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存.并且会强制让缓存了该变量的线程中数据清空,必须从主内存重新读取最新数据,这样就保证了可见性.
有序性
程序执行的顺序按照代码的先后顺序执行就叫做有序性,但是有时候程序的执行并不会遵循.
比如:
int i=1;
int j=2;
这两行代码,程序可能会发生指令重排序,就是代码会重新排列.
为了防止重排序,java提供了很多机制,比如volatile关键字.
volatile不能保证原子性
volatile修饰的变量a做++操作时,实际的指令包括三个(得到a的值,a+1,将a+1赋值给a),因此可能会出现多个线程交叉执行的结果。
public class Test {
private static volatile int a=0;
public static void main(String[] args) {
Thread[] threads=new Thread[5];
for(int i=0;i<5;i++){
threads[i]=new Thread(()->{
for(int j=0;j<10;j++){
try {
System.out.println(++a);
Thread.sleep(1000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threads[i].start();
}
}
}
结果13245678109111213141516171819182020…
出现了相邻或不相邻的两个数字是重复的.这说明volatile不能保证线程的原子性.
使用synchronized修饰,或者Lock锁,或者AtomicInteger
单例模式的双重锁为什么使用volatile
public class Test2 {
public static volatile Test2 test2;
public static Test2 getInstance(){
if(test2==null){
synchronized(Test2.class){
if(test2==null){
test2=new Test2();
}
}
}
return test2;
}
}
为什么这里要加volatile关键字呢?把test=new Test2()拆分,可以分为三步
- memory=allocate();//分配内存
- ctorInstanc(memory);//初始化对象
- test2=memory//设置s指向刚分配的地址
如果没有volatile,可能会发生指令重排序,在编译运行时,从1-2-3排序为1-3-2.此时两个线程同时进来的时候发生了可见性问题,就是一个线程执行了1-3,另一个线程一进来直接返回还未执行2得null对象.而我们volatile关键字已经说过了,可以防止指令重排序.