一、为什么使用volatile
首先,通过一段简单的代码来理解为什么要使用volatile:
1 public class RunThread extends Thread{ 2 private boolean isRunning = true; 3 public boolean isRunning(){ 4 return isRunning; 5 } 6 7 public void setRunning(boolean isRunning){ 8 this.isRunning = isRunning; 9 } 10 11 @Override 12 public void run() { 13 System.out.println("进入run..."); 14 while(isRunning==true){} 15 System.out.println("线程停止了"); 16 } 17 } 18 19 20 21 import java.util.concurrent.TimeUnit; 22 23 public class Run { 24 public static void main(String[] args){ 25 try{ 26 RunThread thread = new RunThread(); 27 thread.start(); 28 TimeUnit.MILLISECONDS.sleep(1000); 29 thread.setRunning(false); 30 System.out.println("赋值为false"); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 } 35 }
执行结果:
我们创建了一个RunThread对象,并启动了该线程。当 isRunning == True时,将一直进行while循环。可以看到,我们启动了线程之后就调用setRunning方法将isRunning设为false。
按道理来说,当isRunning的状态为false时,就会退出while循环,输出“赋值为false”。然后run方法执行结束,线程执行完毕,整个程序运行结束。
但通过执行结果发现,线程进入了while死循环,线程从未终止。
这是由于在启动RunThread.java线程时,变量 private boolean isRunning = True 存在于公共堆栈和线程的私有堆栈中。JVM以server模式运行时,为了提高运行效率,线程一直从私有堆栈中读取isRunning的值为True。而thread.setRunning(false)执行后,是把公共堆栈中的isRunning改为false。所以即便将isRunning改为false,程序依然进入了while死循环。
解决办法:
使用volatile关键字,把 private boolean isRunning = True 改为 volatile private boolean isRunning = True ,再次运行:
volatile的作用是:强制线程从公共堆栈中取变量的值,而非从线程的私有堆栈中取值
二、 volatile的非原子性
1. 虽然volatile关键字增加了实例变量在多个线程之间的可见性,但它不具备同步性,也就是原子性。
1 public class MyThread extends Thread{ 2 volatile public static int count; 3 private static void addCount(){ 4 for(int i =0;i<100;i++){ 5 count++; 6 } 7 System.out.println("count= "+count); 8 } 9 10 @Override 11 public void run() { 12 addCount(); 13 } 14 } 15 16 public class Run { 17 public static void main(String[] args) { 18 MyThread[] mythreadArray = new MyThread[100]; 19 for (int i = 0; i < 100; i++) { 20 mythreadArray[i] = new MyThread(); 21 } 22 for (int i = 0; i < 100; i++) { 23 mythreadArray[i].start(); 24 } 25 } 26 }
执行结果:
volatile保证了线程每次从公共内存中读取变量,保证了同步数据的可见性。但由上述代码可以看出,使用关键字volatile时出现了非线程安全问题。i++ 操作并非一个原子操作,可以分解为:
(1)从内存读取 i 的值;
(2)计算 i 的值;
(3)将 i 的值写入内存
当一个线程正在执行步骤(2)时,另一个线程也可能也在修改 i 值,就会出现脏数据。
2. 解决办法
(1)方法一:使用synchronized关键字
当在 private static void addCount() 前面加上关键字synchronized后 synchronized private static void addCount() ,再次执行:
当时用了synchronized关键字时,就不必再使用volatile关键字了。
(2)方法二:使用原子类进行i++操作
1 public class MyThread extends Thread{ 2 private static AtomicInteger count = new AtomicInteger(0); 3 synchronized private static void addCount(){ 4 for(int i =0;i<100;i++){ 5 count.incrementAndGet(); 6 } 7 System.out.println("count= "+count); 8 } 9 10 @Override 11 public void run() { 12 addCount(); 13 } 14 }
注意:原子类并不完全安全,只有原子类的increamentAndGet( )方法是原子的,不保证方法与方法之间的调用也是原子的。