- volatile
- 保证变量的可见性
- 禁止指令重排
- 不保证原子性
- 如何保证原子性
volatile
-
volatile关键字可以保证变量的可见性。
-
被volatile修饰的变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。
-
无法保证原子性
保证变量的可见性
当多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
如果不能保证可见性的话,会导致其他线程无法及时看到修改的值,可能会导致死循环等问题的出现。
使用volatile
可以保证可见性,及时通知其他线程。当一个共享变量被volatile修饰时,会保证修改的值立即更新到主存,当有其他线程需要读取时,会从内存中读取新值。这是关键的地方。
禁止指令重排
指令重排序是指编译器和运行时对指令作出重新排序的行为。
禁止指令重排序的关键是执行内存屏障。
public native void loadFence(); // 添加读屏障
public native void storeFence(); // 添加写屏障
public native void fullFence(); // 读写屏障
不保证原子性
由于volatile的可见性,可能会导致原子性无法保证。这又是为什么呢?
看下面这段代码:
public class TestDemo2 {
public static class MyTest {
public volatile int num = 0;
public void numPlusPlus() {
num++;
}
}
public static void main(String[] args) {
MyTest myTest = new MyTest();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
for (int j = 0; j < 2000; j++) {
myTest.numPlusPlus();
}
}, "Thread" + i).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
//结果应该是20000
System.out.println(Thread.currentThread().getName() + "\t finally num value is " + myTest.num);
}
}
如果volatile保证原子性的话,结果就应该是20000,但是我们可以看到三次运行的结果是不同的。
注意:
num++;
这一指令其实要执行三个操作:先取值,再自加(tmp),再赋值操作
当多个线程执行numPlusPlus()方法时,线程A读取num=0,执行num+1操作,此时num还没有变化;B线程执行num+1操作。此时A,B保存的num的值都是0,num+1(tmp)的值都为1。
A线程执行赋值的操作,此时,num=1,并将num的值刷新到内存中并通知其他线程保存的num值失效,B读取到num的值为1,而之前tmp保存的结果也为1。
B线程也执行赋值操作num = tmp,结果为1,比预期结果2少了1。
说白了,就是因为volatile不会加锁,多个线程能够同时操作同一变量。
如何保证原子性
使用JUC内部的AtomicInteger
类来保证了我们变量相关的原子性。