“Volatile”的版本间的差异
Jihongchang(讨论 | 贡献)  (建立内容为“volatile 关键字说明”的新页面)  | 
				Jihongchang(讨论 | 贡献)   | 
				||
| 第1行: | 第1行: | ||
| − | volatile   | + | volatile 的使用是为了线程安全,但 volatile 不保证线程安全。  | 
| + | |||
| + | 线程安全有三个要素:可见性、有序性和原子性。  | ||
| + | |||
| + | 线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改发生与预期不符的情况。  | ||
| + | |||
| + | === volatile 的作用 ===  | ||
| + | volatile 有以下三个作用:  | ||
| + | |||
| + | ==== volatile 用于解决多核 CPU cache(高速缓存)导致的变量不同步 ====  | ||
| + | 这本质上是个硬件问题,其根源在于:CPU 的高速缓存的读取速度远远快于主存(物理内存)。  | ||
| + | |||
| + | 所以,CPU 在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显然提高了读取的性能。  | ||
| + | |||
| + | 而多核 CPU 有多个这样的缓存。这就带来了问题,当某个 CPU(例如 CPU1)修改了这个变量(比如把 a 的值从 1 修改为 2 ),但是其他的 CPU(例如 CPU2)在修改前已经把 a=1 读取到自己的缓存了,当 CPU2 再次读取数据的时候,它仍然会去自己的缓存区中读取,此时读取到的值仍然是 1,但是实际上这个值已经变成 2 了。  | ||
| + | |||
| + | 这里,就涉及了线程安全的要素:'''<big>可见性</big>'''。  | ||
| + | |||
| + | 可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能立即看到修改后的值。  | ||
| + | |||
| + | volatile 的实现原理是内存屏障(Memory Barrier),其原理为:当 CPU 写数据时,如果发现一个变量在其他 CPU 中存有副本,那么会发出信号通知其他 CPU 将该副本对应的缓存行置为无效状态,当其他 CPU 读取到 变量副本的时候,会发现该缓存行是无效的,然后,它会从主存重新读取变量。  | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | ==== volatile 可以解决指令重排序的问题 ====  | ||
| + | 一般情况下,程序是按照顺序执行的,例如下面的代码:<syntaxhighlight lang="java" line="1">  | ||
| + |         int i = 0;  | ||
| + |         i++;  | ||
| + |         boolean f = false;  | ||
| + |         f = true;  | ||
| + | </syntaxhighlight>如果 <code>i++</code> 发生在 <code>int i = 0</code> 之前,那么会不可避免地出错,CPU 在执行代码对应指令的时候,会认为 1、2 两行是具备依赖性的,因此,CPU 一定会安排行 1 早于行 2 执行。  | ||
| + | |||
| + | 那么,<code>int i = 0</code> 一定会早于 <code>boolean f = false</code> 吗?  | ||
| + | |||
| + | 并不一定,CPU 在运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。  | ||
| + | |||
| + | 在单线程执行的情况下,发生重排是没有问题的,CPU 保证了顺序不一定一致,但结果一定一致。  | ||
| + | |||
| + | 但在多线程环境下,重排序则会引起很大的问题,这又涉及了线程安全的要素:'''<big>有序性</big>'''  | ||
| + | |||
| + | |||
| + | |||
| + | |||
| + | |||
| + | 有序性是指程序执行的顺序应当按照代码的先后顺序执行。  | ||
| + | |||
| + | 为了更好地理解有序性,下面通过一个例子来分析:<syntaxhighlight lang="java">  | ||
| + | public class TestVolatile {  | ||
| + | |||
| + |     //成员变量i  | ||
| + |     int i = 0;  | ||
| + |     //成员变量f  | ||
| + |     boolean f = false;  | ||
| + | |||
| + |     //线程一的执行代码  | ||
| + |     public void invokeByT1() throws InterruptedException {  | ||
| + |         Thread.sleep(10);  | ||
| + |         i++;  | ||
| + |         f = true;  | ||
| + |     }  | ||
| + | |||
| + |     //线程二的执行代码  | ||
| + |     public void invokeByT2() {  | ||
| + |         while (!f) {  | ||
| + |             System.out.println(i);  | ||
| + |         }  | ||
| + |     }  | ||
| + | |||
| + | }  | ||
| + | </syntaxhighlight>理想的结果应该是  | ||
2023年3月2日 (四) 02:40的版本
volatile 的使用是为了线程安全,但 volatile 不保证线程安全。
线程安全有三个要素:可见性、有序性和原子性。
线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改发生与预期不符的情况。
volatile 的作用
volatile 有以下三个作用:
volatile 用于解决多核 CPU cache(高速缓存)导致的变量不同步
这本质上是个硬件问题,其根源在于:CPU 的高速缓存的读取速度远远快于主存(物理内存)。
所以,CPU 在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显然提高了读取的性能。
而多核 CPU 有多个这样的缓存。这就带来了问题,当某个 CPU(例如 CPU1)修改了这个变量(比如把 a 的值从 1 修改为 2 ),但是其他的 CPU(例如 CPU2)在修改前已经把 a=1 读取到自己的缓存了,当 CPU2 再次读取数据的时候,它仍然会去自己的缓存区中读取,此时读取到的值仍然是 1,但是实际上这个值已经变成 2 了。
这里,就涉及了线程安全的要素:可见性。
可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能立即看到修改后的值。
volatile 的实现原理是内存屏障(Memory Barrier),其原理为:当 CPU 写数据时,如果发现一个变量在其他 CPU 中存有副本,那么会发出信号通知其他 CPU 将该副本对应的缓存行置为无效状态,当其他 CPU 读取到 变量副本的时候,会发现该缓存行是无效的,然后,它会从主存重新读取变量。
volatile 可以解决指令重排序的问题
一般情况下,程序是按照顺序执行的,例如下面的代码:
1        int i = 0;
2        i++;
3        boolean f = false;
4        f = true;
如果 i++ 发生在 int i = 0 之前,那么会不可避免地出错,CPU 在执行代码对应指令的时候,会认为 1、2 两行是具备依赖性的,因此,CPU 一定会安排行 1 早于行 2 执行。
那么,int i = 0 一定会早于 boolean f = false 吗?
并不一定,CPU 在运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。
在单线程执行的情况下,发生重排是没有问题的,CPU 保证了顺序不一定一致,但结果一定一致。
但在多线程环境下,重排序则会引起很大的问题,这又涉及了线程安全的要素:有序性
有序性是指程序执行的顺序应当按照代码的先后顺序执行。
为了更好地理解有序性,下面通过一个例子来分析:
public class TestVolatile {
    //成员变量i
    int i = 0;
    //成员变量f
    boolean f = false;
    //线程一的执行代码
    public void invokeByT1() throws InterruptedException {
        Thread.sleep(10);
        i++;
        f = true;
    }
    //线程二的执行代码
    public void invokeByT2() {
        while (!f) {
            System.out.println(i);
        }
    }
}
理想的结果应该是