volatile关键字
1.volatile关键字的可见性
要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1.线程A把工作内存A中的更新过的共享变量刷新到主内存中去。
2.线程B到主内存中去读取线程A刷新过的共享变量,然后copy一份到工作内存B中去。
Java内存模型是围绕着并发编程中原子性、可见性、有序性这三个特征来建立的:https://www.cnblogs.com/lewis0077/p/5143268.html
public class VolatileTest extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean running) { this.isRunning = running; } @Override public void run() { System.out.println("进入到run方法"); while (isRunning) { } System.out.println("run方法结束"); } }
public class Run { public static void main(String[] args) { try { VolatileTest thread = new VolatileTest(); thread.start(); Thread.sleep(1000); thread.setRunning(false); } catch (InterruptedException e) { e.printStackTrace(); } } }
main线程 将启动的线程RunThread中的共享变量设置为false,从而想让VolatileTest.java 的while循环结束。
如果,我们使用JVM -server参数执行该程序时,RunThread线程并不会终止!从而出现了死循环!!
原因分析:
现在有两个线程,一个是main线程,另一个是RunThread。它们都试图修改 第三行的 isRunning变量。按照JVM内存模型,main线程将isRunning读取到本地线程内存空间,修改后,再刷新回主内存。
而在JVM 设置成 -server模式运行程序时,线程会一直在私有堆栈中读取isRunning变量。因此,RunThread线程无法读到main线程改变的isRunning变量
从而出现了死循环,导致RunThread无法终止。这种情形,在《Effective JAVA》中,将之称为“活性失败”
解决方法,在第三行代码处用 volatile 关键字修饰即可。这里,它强制线程从主内存中取 volatile修饰的变量。
// private boolean isRunning = true; private volatile boolean isRunning = true;
扩展一下,当多个线程之间需要根据某个条件确定 哪个线程可以执行时,要确保这个条件在 线程 之间是可见的。因此,可以用volatile修饰。
综上,volatile关键字的作用是:使变量在多个线程间可见(可见性)
2.volatile关键字的非原子性
所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。
比如,变量的自增操作 i++,分三个步骤:
①从内存中读取出变量 i 的值
②将 i 的值加1
③将 加1 后的值写回内存
这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。
关于volatile的非原子性,看个示例:
public class VolatileAtomicTest extends Thread{ public static volatile int count; private static void addCount() { for (int i = 0; i < 100; i++) { count++; } System.out.println("count=" + count); } @Override public void run() { addCount(); } }
public class Run { public static void main(String[] args) { Run run = new Run(); run.testAtomic(); } private void testAtomic(){ VolatileAtomicTest[] threads = new VolatileAtomicTest[100]; for (int i = 0; i<100;i++){ threads[i] = new VolatileAtomicTest(); } for (int i = 0; i<100;i++){ threads[i].start(); } } }
期望的正确的结果应该是 100*100=10000,但是,实际上count可能并没有达到10000
原因是:volatile修饰的变量并不保证对它的操作(自增)具有原子性。(对于自增操作,可以使用JAVA的原子类AutoicInteger类保证原子自增)
比如,假设 i 自增到 5,线程A从主内存中读取i,值为5,将它存储到自己的线程空间中,执行加1操作,值为6。此时,CPU切换到线程B执行,从主从内存中读取变量i的值。由于线程A还没有来得及将加1后的结果写回到主内存,线程B就已经从主内存中读取了i,因此,线程B读到的变量 i 值还是5
相当于线程B读取的是已经过时的数据了,从而导致线程不安全性。这种情形在《Effective JAVA》中称之为“安全性失败”
综上,仅靠volatile不能保证线程的安全性。(原子性)
此外,volatile关键字修饰的变量不会被指令重排序优化。volatile 修饰的变量会禁止指令重排序(有序性)
3. volatile 与 synchronized 的比较
volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
关于synchronized,可参考:JAVA多线程之Synchronized关键字--对象锁的特点
比较:
①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
4. 线程安全性
线程安全性包括两个方面,①可见性。②原子性。
从上面自增的例子中可以看出:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
关于Synchronized底层实现原理,参考:https://blog.csdn.net/javazejian/article/details/72828483