volatile的特性代码验证
一 . 可见性(visibility)
volatile关键字修饰的变量,如果值发生了改变,其他线程会立刻获取到,从而避免了出现脏读的情况。
1 public class TestVolatile { 2 3 public static void main(String[] args) { 4 MyData myData = new MyData(); 5 new Thread(new Runnable() { 6 @Override 7 public void run() { 8 System.out.println("进入操作数据线程"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 //调用方法 赋值 15 myData.changeData(); 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"操作数据线程").start(); 19 20 // 主线程查看数据是否改了 21 while (myData.data == 0){ 22 23 } 24 System.out.println("main线程结束"); 25 } 26 } 27 28 class MyData{ 29 int data = 0; 30 public void changeData(){ 31 this.data = 2020; 32 } 33 34 }
如上面代码,有两个线程在操作MyDdata数据类,看一下执行结果
从结果可以看出,main线程一直就没有获取到数据更新信息,内存中的数据存储用图直观的看一下
main线程的内存线程并没获取到数据更新。
下面变量加上volatile的效果
1 public class TestVolatile { 2 3 public static void main(String[] args) { 4 MyData myData = new MyData(); 5 new Thread(new Runnable() { 6 @Override 7 public void run() { 8 System.out.println("进入操作数据线程"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 //调用方法 赋值 15 myData.changeData(); 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"操作数据线程").start(); 19 20 // 主线程查看数据是否改了 21 while (myData.data == 0){ 22 23 } 24 System.out.println("main线程结束"); 25 } 26 } 27 28 class MyData{ 29 volatile int data = 0; 30 public void changeData(){ 31 this.data = 2020; 32 } 33 34 }
看一下执行结果
发现main方法已经获取到了数据更新。从而验证了volatile的可见性。
二 . 无法保证原子性
直接上代码
1 public class TestVolatile1 { 2 3 public static void main(String[] args) { 4 MyData1 myData = new MyData1(); 5 6 new Thread(new Runnable() { 7 @Override 8 public void run() { 9 //调用方法 赋值 10 myData.changeData(); 11 for(int i = 0;i < 9999;i++) { 12 myData.changeData(); 13 } 14 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 15 } 16 },"线程1").start(); 17 18 19 new Thread(new Runnable() { 20 @Override 21 public void run() { 22 //调用方法 赋值 23 myData.changeData(); 24 for(int i = 0;i < 9999;i++) { 25 myData.changeData(); 26 } 27 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 28 } 29 },"线程2").start(); 30 31 new Thread(new Runnable() { 32 @Override 33 public void run() { 34 //调用方法 赋值 35 myData.changeData(); 36 for(int i = 0;i < 9999;i++) { 37 myData.changeData(); 38 } 39 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 40 } 41 },"线程3").start(); 42 43 new Thread(new Runnable() { 44 @Override 45 public void run() { 46 //调用方法 赋值 47 myData.changeData(); 48 for(int i = 0;i < 9999;i++) { 49 myData.changeData(); 50 } 51 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 52 } 53 },"线程4").start(); 54 55 while (Thread.activeCount() > 1) { 56 Thread.yield(); 57 } 58 59 System.out.println("最终数值 :"+myData.data); 60 } 61 } 62 63 class MyData1{ 64 volatile int data = 0; 65 public void changeData(){ 66 data++; 67 } 68 69 }
咱们可以预测一下,如果正常的话,咱们应该得到的最终数据应该是40000 ,但结果如下
可以看到最终数据并不是我们想要的结果,多线程同时操作volatile修饰变量,无法保证数据的原子性。
那如何解决这个问题呢,用sychornized,可以处理,但是这是重量级锁,不推荐使用,还可以用 AtomicInteger 来处理这个情况实现代码如下
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class TestVolatile1 { 4 5 public static void main(String[] args) { 6 MyData1 myData = new MyData1(); 7 8 new Thread(new Runnable() { 9 @Override 10 public void run() { 11 //调用方法 赋值 12 myData.changeData(); 13 for(int i = 0;i < 9999;i++) { 14 myData.changeData(); 15 } 16 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 17 } 18 },"线程1").start(); 19 20 21 new Thread(new Runnable() { 22 @Override 23 public void run() { 24 //调用方法 赋值 25 myData.changeData(); 26 for(int i = 0;i < 9999;i++) { 27 myData.changeData(); 28 } 29 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 30 } 31 },"线程2").start(); 32 33 new Thread(new Runnable() { 34 @Override 35 public void run() { 36 //调用方法 赋值 37 myData.changeData(); 38 for(int i = 0;i < 9999;i++) { 39 myData.changeData(); 40 } 41 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 42 } 43 },"线程3").start(); 44 45 new Thread(new Runnable() { 46 @Override 47 public void run() { 48 //调用方法 赋值 49 myData.changeData(); 50 for(int i = 0;i < 9999;i++) { 51 myData.changeData(); 52 } 53 System.out.println(Thread.currentThread().getName()+" : "+myData.data); 54 } 55 },"线程4").start(); 56 57 while (Thread.activeCount() > 1) { 58 Thread.yield(); 59 } 60 61 System.out.println("最终数值 :"+myData.data); 62 } 63 } 64 65 class MyData1{ 66 AtomicInteger data = new AtomicInteger(); 67 public void changeData(){ 68 data.getAndIncrement(); 69 } 70 71 }
执行结果如下
如此数据原子性问题便解决了。
三 . 指令重排
在JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对指令顺序进行重新排序。在不改变程序执行结果的前提下,优化程序的运行效率(不改变单线程下的程序执行结果)
看一段简单的代码
1 public class Data { 2 3 int a = 1; //步骤1 4 int b = 2; //步骤2 5 int c = a+b; //步骤3 6 7 }
单线程下,代码执行结果c的结果3,但是在执行的过程时候并不一定是1 , 2,3这个执行顺序,在发生指令重排后,可能是2,1,3。单线程下对工程并没有什么影响。
但是如果是多线程,就会出现问题。查看如下方法
1 public class Volatile { 2 3 int a = 1; 4 boolean flag = false; 5 6 public void dosome1() { 7 a = 2;// 步骤1 8 flag = true; //步骤2 9 } 10 11 public void dosome2() { 12 if(flag){ 13 int b = a+a; // 步骤3 14 } 15 } 16 }
上面的代码步骤3其实是两个步骤,为了好理解,可以看成为一个步骤。
如果线程A 操作dosome1 而线程而B 操作dosome2 如果不发生指令重排
可能顺序可能是 1,2,3 b=4 ,这也是我们期望的,
还会出现以下顺序
1,3,2 3,1,2 这两种可能性,如果是这两个,代表不符合条件,没有声明b变量。
但是如果发生重排后,因为1,2没有依赖关系,很有可能发生指令重排,那名执行的结果就可能出现以下顺序
2,3,1 如果出现这个顺序,就会声明变量b,结果为2;这个结果就会很恐怖了,就好比我们做了一个工程,每次执行的结果无法确定。这必然是不行的。为了解决这个问题,我们便可以用volatile来修饰变量。当然sychornized也可以解决。
重排是个比较麻烦的过程,这是一个简单理解,后续再做详细的探讨。