一、volatile解决了什么问题?
对于以下代码:
private static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag) ;
System.out.println("thread 1 out");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("thread 2 out");
});
t1.start();
t2.start();
}
按照正常的逻辑,当线程2修改了flag
的值之后,线程1就会退出死循环,从而打印出thread 1 out
这句话。实际上运行程序,在打印出thread 2 out
这句话之后,程序就会陷入死循环,并不会再打印线程1中的句子。这就是因为线程1读取flag
值时,是读取的线程内保存的变量副本的值,这就导致了虽然flag
已经修改为了false
,但是线程1读到的值仍然是true
,也就不会退出循环了。
要解决这个问题,只需要把flag
变量加上volatile修饰符即可。
private static volatile boolean flag = true;
再运行程序,就会发现可以正常打印出线程1中的句子了。volatile解决的就是类似的在多线程环境下变量的可见性问题。
二、volatile的作用是什么?
volatile关键字主要有两点作用:保证可见性、禁止重排序。
1. 保证可见性
对于volatile修饰的变量,在读值时会直接读取主内存的值而不是工作线程中的变量副本,在写入时也会刷新主内存中的值,这也就保证了可见性。
2. 禁止重排序
对于一些代码,虚拟机会在不影响结果的前提下对字节码进行重排序,从而提高效率。不影响结果是在单线程的环境下,对于多线程
三、volatile能保证线程安全或者原子性吗?比如i++,能使用volatile来确保线程安全吗?
volatile能够保证共享变量读或写单步的可见性。但是对于i++
来说,并不能使用volatile来保证线程安全。
参照如下代码:
private static int i = 0;
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int j = 0; j < 1000; j++) {
i++;
Thread.yield();
}
};
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(task);
exec.submit(task);
exec.shutdown();
exec.awaitTermination(60, TimeUnit.SECONDS); // 等待任务执行完毕
System.out.println(i);
}
通过查看i++
的字节码,可以发现它其实是分为4步:
其中,getstatic
将静态变量i
的值入栈,iconst_1
将1入栈,iadd
将栈顶两个数出栈、并将它们的和入栈,putstatic
将栈顶的值赋给变量i
。
通过字节码就可以知道为什么i++
是线程不安全的:如果两个线程同时执行i++
这句代码,有可能线程1将i
值入栈之后,线程2抢到CPU时间片,线程2又将i
的值入栈——这时候i
的值还是原始值,并未改变。这种情况下,执行了两次i++
后,i
的新值本应是i + 2
,结果却是i + 1
。
而volatile——保证可见性与禁止重排序——在这个问题上显得毫无用处。所以,volatile并不可以保证线程安全。正确的做法是加锁或者使用Atomic类。
网友评论