作用
- 保证可见性;
- 防止指令重排;
- 但是不保证原子性;
可见性验证
可见性:在JMM(java memory model)java内存模型中,其他线程从主内存空间把值拷贝到自己的工作空间,线程修改之后的值会返回给主内存,主内存会通知其他线程,此为可见性。若线程修改之后的值未返回给主内存或主内存为通知其他线程即为不可见性
代码
public class VolatileTest {
public static void main(String[] args) {
Key key = new Key();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("进入操作数据线程");
// 线程休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 调用方法 改变flag的值
key.turnFalse();
System.out.println("值改变: "+key.flag);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程结束");
}
});
thread.start();
// 若线程更改flag生效,主线程会结束
while (key.flag){
}
System.out.println("main线程结束");
}
}
class Key {
public boolean flag = true;
public void turnFalse(){
this.flag = false;
}
}
运行结果
代码
将Key的flag的关键字加上volatile下
public volatile boolean flag = true
运行结果
结论
按照预想设计的逻辑,一旦线程更改的flag的值,while循环就会立即结束,可在未加volatile的程序中,while陷入了死循环,这代表while当时不可见flag的变化
提醒
volatile使得线程可以拷贝到正确的值,但static关键字可以让变量只存在一项实例,但实际上依然不能解决问题
static和volatile的区别
- volatile是告诉编译器,每次取这个变量的值都需要从主存中取,而不是用自己线程工作内存中的缓存.
- static 是说这个变量,在主存中所有此类的实例用的是同一份,各个线程创建时需要从主存同一个位置拷贝到自己工作内存中去(而不是拷贝此类不同实例中的这个变量的值),也就是说只能保证线程创建时,变量的值是相同来源的,运行时还是使用各自工作内存中的值,依然会有不同步的问题
例
代码
class VolatileTest {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("进入操作数据线程");
// 线程休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 调用方法 改变flag的值
Key.turnFalse();
System.out.println("值改变: "+Key.flag);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程结束");
}
});
thread.start();
// 若线程更改flag生效,主线程会结束
while (Key.flag){
}
System.out.println("main线程结束");
}
}
class Key {
public static boolean flag = true;
public static void turnFalse(){
Key2.flag = false;
}
}
运行结果
结论:主线程依然不可见flag的改变
防止指令重拍的验证
验证指令重拍现象
代码
class Test{
static int a;
static int b;
static int y;
static int x;
public static void main(String[] args) throws InterruptedException {
int count = 0;
while (true) {
a = 0;
b = 0;
y = 0;
x = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
count++;
if (x == 0 && y == 0) {
System.out.println("程序发生了指令重排,第" + count + "次");
break;
}
}
}
}
定义了四个静态变量abxy,然后在一个死循环中两个线程分别对这四个变量进行赋值。
若程序不会发生指令重排,那么a = 1; 一定先于 x = b; 执行,b = 1; 一定先于 y = a; 执行,那么两个线程并发执行的排列组合就是C(4,2)=6 种组合的公式参考,如下
执行顺序 | 执行后x的值 | 执行后y的值 |
---|---|---|
a=1,x=b,b=1,y=a | 0 | 1 |
a=1,b=1,x=b,y=a | 1 | 1 |
a=1,b=1,y=a,x=b | 1 | 1 |
b=1,y=a,a=1,x=b | 1 | 0 |
b=1,a=1,y=a,x=b | 1 | 1 |
b=1,a=1,x=b,y=a | 1 | 1 |
根据表格可知,x与y不可能同时为0,即循环不可能停止,但实际上程序停住了,如下图
结论
存在指令重排现象
使用volatile确实可以防止指令重拍现象,但不便验证,以此程序为例就是程序确实陷入了死循环
无法保证原子性
代码
class Test {
public static void main(String[] args) throws Exception {
Thread t1 = new Part();
Thread t2 = new Part();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(Part.a);
}
}
class Part extends Thread {
volatile static int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
运行结果
不一定都是这么多,肯定在10000~20000之间(确实是有可能20000)
结论
若volatile可以保证原子性,那么结果肯定每次都是20000,但实际上大多数时候都是小于20000的,故而确定volatile不能保证原子性
注:若若想保证原子性还是需要synchronized
或lock
亦或是其它方法