volatile关键字 2020-03-14 16:17 > 注意:本文引用的内容已经发现某些地方有错误,还未删改。请勿阅读。正确的内容请见:http://riun.xyz/work/183 *参考链接: [http://ifeve.com/java-volatile%e5%85%b3%e9%94%ae%e5%ad%97/](http://ifeve.com/java-volatile关键字/)* > volatile不能完整的保证变量的可见性,volatile修饰一个变量表示的是这样: 每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。 > > 所以只适应于部分情况。一个线程修改了变量,另一个线程读取变量,那么将变量声明为volatile,就能保证写入变量后,对另一个现成的读取是可见的。 > > 但如果多个线程都修改了变量的值,主存中也能存储正确的值,但这有一个前提:变量新值的写入不能依赖旧值。如果依赖旧值,那么多个线程对变量的修改就可以覆盖,导致最终得不到正确结果。 ### 总述 在 **JDK1.2** 之前,Java 的内存模型实现总是**从主存**(即共享内存)**读取变量**,是不需要进行特别的注意的。而在**当前**的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的**寄存器**)中,而不是直接在主存中进行读写。**这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝**,造成数据的不一致。 Java的volatile关键字用于标记一个变量“应当存储在主存”。更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。 从JDK1.5开始, volatile关键字除了保证volatile变量从主存读写外,还提供了更多的保障。 比如①提供了**完整的volatile可见性**保证: - 如果线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有可见的变量值,在线程B读取volatile变量后也同样是可见的。 - 如果线程A读取一个volatile变量,那么线程A中所有可见的变量也会同样从主存重新读取。 即:若在修改volatile变量之前修改了其他变量,那么当对volatile变量修改时,会将其他非volatile变量一起更新至主存;当读取volatile变量时,会一起读取其他非volatile变量。因此可以使用volatile变量来保证程序中其他变量的可见性:**在读取一系列变量值时先读取volatile变量,在写入一系列变量值时最后写入volatile变量。** 例如: ```java public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days;//读取volatile变量,同时从主存中读取其他所有变量的值 total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days;//写入volatile变量,同时强制将其他所有变量的值写入主存 } } ``` ②提供**happens-before原则**为volatile关键字的可见性提供强制保证,避免了指令重排可能带来的隐患: - 如果有读写操作发生在写入volatile变量之前,读写其他变量的指令不能重排到写入volatile变量之后。 - 如果有读写操作发生在读取volatile变量之后,读写其他变量的指令不能重排到读取volatile变量之前。 即在未重排时能够受到volatile变量带来的“有益”影响的变量不能失去这份影响。而在未重排时没有得到volatile变量带来的“有益”影响的变量可以通过重排得到这份影响。 ### 变量可见性问题 Java的volatile关键字能保证变量修改后,对其他线程是可见的。而普通变量(非volatile变量)通常是不可见的。 在一个多线程的应用中,线程在操作普通变量时,出于性能考虑,每个线程可能会将变量从主存拷贝到CPU缓存中(**可以理解为寄存器**)。如果你的计算机有多个CPU,每个线程可能会在不同的CPU中运行。这意味着,每个线程都有可能会把变量拷贝到各自CPU的缓存中。 ![](https://minio.riun.xyz/riun1/2022-06-25_qz4brsnU71mFjnSoFq.jpg) 如果有两个以上的线程访问一个共享对象,这个共享对象包含一个counter变量 ```java public class SharedObject { public int counter = 0; } ``` 如果只有线程1修改了(自增)counter变量( counter的值不保证会从CPU缓存写回到主存中 ),而线程1和线程2两个线程都会在某些时刻读取counter变量。 此时, CPU缓存和主存中的counter变量值并不一致 。 ![](https://minio.riun.xyz/riun1/2022-06-25_qzyYKbN69n6dUlQ0Nm.jpg) 这就是“可见性”问题,线程看不到变量最新的值,因为其他线程还没有将变量值从CPU缓存写回到主存。一个线程中的修改对另外的线程是不可见的。 ### volatile可见性保证 > volatile的可见性保证存在限制:多个线程同时写入volatile变量的值时,变量新值的写入不能依赖变量的旧值 Java的volatile关键字就是设计用来解决变量可见性问题。 将counter变量声明为volatile,则在写入counter变量时,也会同时将变量值写入到主存中。同样在读取counter变量值时,也会直接从主存中读取。 ```java public class SharedObject { public volatile int counter = 0; } ``` 将一个变量声明为volatile,可以保证变量写入时对其他线程的可见。 在上面的场景中,一个线程(T1)修改了counter,另一个线程(T2)读取了counter(但没有修改它),将counter变量声明为volatile,就能保证写入counter变量后,对T2是可见的。 然而,如果T1和T2都修改了counter的值,只是将counter声明为volatile还远远不够。 ### volatile的缺陷 volatile能保证所有读取都是从主存中读取,所有写入都是直接写入到主存。但是此情况下还是不能适用于所有多线程并发情况。像前面说的: **volatile的可见性保证存在限制:多个线程同时写入volatile变量的值时,变量新值的写入不能依赖变量的旧值** 如果线程需要先读取一个volatile变量的值,在此基础上计算一个新的值,那么volatile变量就不能保证可见性。 多个线程同时读取volatile变量的值,然后以此计算出了新的值,这时各个线程往主存中写值时,会存在覆盖。 多个线程对counter变量进行自增操作就是这样的情形。 设想一下,如果线程1将共享变量counter的值0读取到它的CPU缓存,然后自增为1,而还没有将新值写回到主存。线程2这时从主存中读取的counter值依然是0,依然放到它自身的CPU缓存中,然后同样将counter值自增为1,同样也还没有将新值写回到主存。 ![](https://minio.riun.xyz/riun1/2022-06-25_qzMWubZp6dhve9Y3P7.jpg) 从实际的情况来看,线程1和线程2现在就是不同步的。共享变量counter正确的值应该是2,但各个线程中CPU缓存的值都是1,而主存中的值依然是0。这是很混乱的。即使线程最终将共享变量counter的值写回到主存,那值也明显是错的。 ### volatile使用场景 如果只有**一个线程**对volatile进行**读写**,而**其他线程**只是**读取**变量,这时,对于只是读取变量的线程来说,volatile就已经可以保证读取到的是变量的最新值。如果没有把变量声明为volatile,这就无法保证。 ### volatile的性能考量 读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,你应当只在需要保证变量可见性的情况下,才使用volatile变量。 ## 普通 RunThread.java ```java package VolatileDemo; /** * @author: HanXu * on 2019/12/20 * Class description: */ public class RunThread extends Thread { private boolean sign = true; int m; public boolean isSign() { return sign; } public void setSign(boolean sign) { this.sign = sign; } @Override public void run() { System.out.println(this.getName() + "---------------start--------------"); while (sign){ int a = 1; int b = 2; m = a + b; } System.out.println(this.getName() + m); System.out.println(this.getName() + "---------------end--------------"); } } ``` Test.java ```java package VolatileDemo; /** * @author: HanXu * on 2019/12/20 * Class description: */ public class Test { public static void main(String[] args) throws InterruptedException { RunThread thread = new RunThread(); thread.start(); Thread.sleep(3000); thread.setSign(false); System.out.println("赋值为false"); } } ``` 测试结果: ![](https://minio.riun.xyz/riun1/2022-06-25_qA4tQxkpcT7IaL5jQh.jpg) 主线程执行了thread.setSign(false);将sign的值修改为false,那么线程Thread-0应该停止执行while循环,并输出m和“end”。但是并没有,程序就这样一直运行。 因为在while循环中,Thread-0有任务要执行(循环),所以一旦Thread-0线程拿到cpu资源,就会立即执行while循环中的代码,其从缓存中拿到的sign仍是true。 ![](https://minio.riun.xyz/riun1/2022-06-25_qAx0l5o4obN8xPJ7yC.jpg) **而将sign变量改为volatile变量** ```java private volatile boolean sign = true; ``` 测试结果: ![](https://minio.riun.xyz/riun1/2022-06-25_qAI6vJkZFLfqAqfBjG.jpg) 当主线程将sign的值修改为false后,线程Thread-0每次取值都是从主存中取,拿到了正确的值,所以就会停止循环。 ![](https://minio.riun.xyz/riun1/2022-06-25_qASwUFpwK8t9huOuWY.jpg) **这里也可以使用输出语句或睡眠函数,都可以让sign的值得到及时更新**,即使未使用volatile修饰。 上述RunThread.java修改为: ```java package VolatileDemo; /** * @author: HanXu * on 2019/12/20 * Class description: */ public class RunThread extends Thread { private boolean sign = true; int m; public boolean isSign() { return sign; } public void setSign(boolean sign) { this.sign = sign; } @Override public void run() { System.out.println(this.getName() + "---------------start--------------"); while (sign){ int a = 1; int b = 2; m = a + b; System.out.println(this.getName() + "我让JVM有了休息的机会,去更新值"); } System.out.println(this.getName() + m); System.out.println(this.getName() + "---------------end--------------"); } } ``` ![](https://minio.riun.xyz/riun1/2022-06-25_qB4kMoRrVkkjzTVLfL.jpg) 因为:JVM会尽力保证内存的可见性,即便这个变量没有加同步关键字。换句话说,只要CPU有时间,JVM会尽力去保证变量值的更新。这种与volatile关键字的不同在于,volatile关键字会强制的保证线程的可见性。而不加这个关键字,JVM也会尽力去保证可见性,但是如果CPU一直有其他的事情在处理,它也没办法。最开始的代码,一直处于死循环中,CPU处于一直占用的状态,这个时候CPU没有时间,JVM也不能强制要求CPU分点时间去取最新的变量值。而加了输出或者sleep语句之后,CPU就有可能有时间去保证内存的可见性,于是while循环可以被终止。 --END--
发表评论