java线程一之volatile
1. 什么叫线程安全?
当多个线程访问某个类时,这个类始终能表现出正确的行为,我们称这个类是线程安全的
2. Java内存模型,也就是java线程是如何存取共享变量的
Java的所有变量都存储在主内存中,每个线程都有自己独立的工作内存,每创建一个线程,则会为该线程分配一个独立的工作内存,线程与线程之间的工作内存不可相互调用。
线程在操作变量时,会从主内存中将变量拷贝到自己的工作内存中,并在自己的工作内存中对变量进行所有的操作,当操作完变量以后,会在一个不确定的时间,将变量更新到主内存中。其它线程再从主内存中将变量拷贝到自己的工作内存中继续操作。
就是通过对主内存的拷贝与更新的方式,来保证线程与线程之间存取共享变量的。
看下图:JMM(java memory model)即java内存模型
3. 线程安全的三个概念
a) 原子性
一个或多个操作,要么全部执行,要么全部失败。
举例:A向B转1000块钱。
这里面包含两个动作:A减1000块钱,B加1000块钱,如果在A减1000块钱后,程序出错,或者中断了,则取消A的操作,保证A与B的动作要一致。
b) 可见性
一个线程更改了共享变量,其它的线程能够立即看到新修改的值。
举个例子:
//线程1
Int i=0;
i=10;
//线程2
j=i;
当线程1执行i=10时,这时候是在线程1 自己的工作内存中,还没到更新到主内存,这时候执行线程2,那么线程2的j取的i的值就可能是0。
这就是可见性的问题,线程1对变量进行修改了以后,线程2没有立即能看到线程1修改的值。
c) 有序性
程序执行的顺序按照代码的先后顺序执行。
如下面的例子:
Int a=0; //语句1
Int b=0; //语句2
a=1; //语句3
b=2; //语句4
我们一定认为程序一定是按照“语句1—语句2—语句3—语句4”执行的,但事实并非如此,这儿有可能发生指令重排序。
什么叫指令重排序:处理器为了提高程序的运行效率,可能会对输入代码进行优化。它不保证执行代码指令的顺序和代码中的顺序一致,但它能保证最后的执行结果和代码中的执行结果是一致的。
所以上述的代码也有可能执行的顺序是:“语句1—语句2—语句4—语句3”
那么指令重排序它是如何保证最后的执行结果与代码中的执行结果是一致的呢?
因为重排序时,处理器会考虑指令之间的数据依赖性,如果一个指令A必须用到指令B的结果,那么即使重排序了,B也必定会在A之前执行。
如下面的例子:
Int a=0; //语句1
Int b=0; //语句2
a=3; //语句3
b=a*a; //语句4
这儿的执行顺序,语句3一定是在语句4之前执行的,因为语句4依赖语句3.
因为重排序最后的执行结果是正确的,所以在单线程中是完全没有问题的,但是在多线程中就有问题了。如下面的例子:
//线程1
context=loadContext(); //语句1
a=true; //语句2
//线程2
If(!a){
Sleep();
}
doSometing(context);
按照重排序如果是先执行了线程1的语句2,那么这时候执行线程2,这时候的context还是空,这儿就会发生空指针异常
综上所述,要想程序正确的并发执行,必须要保证程序的原子性,可见性与有序性。
4. Volatile的作用
a) 保证了可见性,因为用volatile修饰的变量会立即变更到主内存中
还是前面的例子:
//线程1
Volatile Int i=0;
i=10;
//线程2
j=i;
我们在线程1的变量i之前加上了volatile修饰符,这样当线程1执行i=10时,这时候volatile会强制将修改的i值存入主内存,这时候线程2的j取的i值就是主内存中的10了
b) 保证了一定程度上的有序性,因为在进行重排序时,被volatile修饰的语句,不会被重排序
如下的例子:
A=2; //语句1
B=3; //语句2
Volatile x=1; //语句3
A=4; //语句4
B=5; //语句5
被volatile修饰的语句3不会在语句1,语句2之前,也不会在语句4,语句5之后。
之前的例子:
//线程1
context=loadContext(); //语句1
volatile Boolean a=true; //语句2
//线程2
If(!a){
Sleep();
}
doSometing(context);
我们在线程1的语句2添加volatile修饰符,这样就能保证在执行语句2的时候,语句1必定执行完毕,这样就不会出现之前的问题
5. volatile的使用场景是对修饰的变量的操作必须是原子性的操作
如下面的例子:
Volatile a=10;
doSomething(){
if(a==50){
doSomething();
a++;
}
}
上面这个例子的volatile就是无效的,因为它不是原子操作,当a==50时,线程A执行这个方法,a==50是true,A还没有执行a++;这时候线程B也正好执行这个方法,a==50也是true,那么线程A与B执得到的a值都是等于51,这样数据就不正确了。