volite关键字
volatile关键字
作用是使变量在多个线程间可见。
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。
一个变量被定义为volatile后,它将具备两种特性:
1、保证此变量对所有线程的"可见性",所谓"可见性"是指当一条线程修改了这个变量的值,新值对于其它线程来说都是可以立即得知的,而普通变量不能做到这一点,普通变量的值在在线程间传递均需要通过主内存来完成,再强调一遍,volatile只保证了可见性,并不保证基于volatile变量的运算在并发下是安全的
2、使用volatile变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
总结一下Java内存模型对volatile变量定义的特殊规则:
1、在工作内存中,每次使用某个变量的时候都必须线从主内存刷新最新的值,用于保证能看见其他线程对该变量所做的修改之后的值
2、在工作内存中,每次修改完某个变量后都必须立刻同步回主内存中,用于保证其他线程能够看见自己对该变量所做的修改
3、volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序顺序相同
直接先举一个例子:
1 public class MyThread28 extends Thread 2 { 3 private boolean isRunning = true; 4 5 public boolean isRunning() 6 { 7 return isRunning; 8 } 9 10 public void setRunning(boolean isRunning) 11 { 12 this.isRunning = isRunning; 13 } 14 15 public void run() 16 { 17 System.out.println("进入run了"); 18 while (isRunning == true){} 19 System.out.println("线程被停止了"); 20 } 21 }
1 public static void main(String[] args) 2 { 3 try 4 { 5 MyThread28 mt = new MyThread28(); 6 mt.start(); 7 Thread.sleep(1000); 8 mt.setRunning(false); 9 System.out.println("已赋值为false"); 10 } 11 catch (InterruptedException e) 12 { 13 e.printStackTrace(); 14 } 15 }
看一下运行结果:
进入run了 已赋值为false
也许这个结果有点奇怪,明明isRunning已经设置为false了, 线程还没停止呢?
这就要从Java内存模型(JMM)说起,这里先简单讲,虚拟机那块会详细讲的。根据JMM,Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。
出现打印结果现象的原因就是主内存和工作内存中数据的不同步造成的。因为执行run()方法的时候拿到一个主内存isRunning的拷贝,而设置isRunning是在main函数中做的,换句话说 ,设置的isRunning设置的是主内存中的isRunning,更新了主内存的isRunning,线程工作内存中的isRunning没有更新,当然一直死循环了,因为对于线程来说,它的isRunning依然是true。
解决这个问题很简单,给isRunning关键字加上volatile。加上了volatile的意思是,每次读取isRunning的值的时候,都先从主内存中把isRunning同步到线程的工作内存中,再当前时刻最新的isRunning。看一下给isRunning加了volatile关键字的运行效果:
进入run了 已赋值为false 线程被停止了
看到这下线程停止了,因为从主内存中读取了最新的isRunning值,线程工作内存中的isRunning变成了false,自然while循环就结束了。
volatile的作用就是这样,被volatile修饰的变量,保证了每次读取到的都是最新的那个值。线程安全围绕的是可见性和原子性这两个特性展开的,volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性。
多提一句,synchronized除了保障了原子性外,其实也保障了可见性。因为synchronized无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。
原子类也无法保证线程安全
原子操作表示一段操作是不可分割的,没有其他线程能够中断或检查正在原子操作中的变量。一个原子类就是一个原子操作可用的类,它可以在没有锁的情况下保证线程安全。
但是这种线程安全不是绝对的,在有逻辑的情况下输出结果也具有随机性,比如
1 public class ThreadDomain29 2 { 3 public static AtomicInteger aiRef = new AtomicInteger(); 4 5 public void addNum() 6 { 7 System.out.println(Thread.currentThread().getName() + "加了100之后的结果:" + 8 aiRef.addAndGet(100)); 9 aiRef.getAndAdd(1); 10 } 11 }
1 public class MyThread29 extends Thread 2 { 3 private ThreadDomain29 td; 4 5 public MyThread29(ThreadDomain29 td) 6 { 7 this.td = td; 8 } 9 10 public void run() 11 { 12 td.addNum(); 13 } 14 }
1 public static void main(String[] args) 2 { 3 try 4 { 5 ThreadDomain29 td = new ThreadDomain29(); 6 MyThread29[] mt = new MyThread29[5]; 7 for (int i = 0; i < mt.length; i++) 8 { 9 mt[i] = new MyThread29(td); 10 } 11 for (int i = 0; i < mt.length; i++) 12 { 13 mt[i].start(); 14 } 15 Thread.sleep(1000); 16 System.out.println(ThreadDomain29.aiRef.get()); 17 } 18 catch (InterruptedException e) 19 { 20 e.printStackTrace(); 21 } 22 }
这里用了一个Integer的原子类AtomicInteger,看一下运行结果:
Thread-1加了100之后的结果:200 Thread-4加了100之后的结果:500 Thread-3加了100之后的结果:400 Thread-2加了100之后的结果:300 Thread-0加了100之后的结果:100 505
显然,结果是正确的,但不是我们想要的,因为我们肯定希望按顺序输出加了之后的结果,现在却是200、500、400、300、100这么输出。导致这个问题产生的原因是aiRef.addAndGet(100)和aaiRef.addAndGet(1)这两个操作是可分割导致的。
解决方案,就是给addNum方法加上synchronized即可。
synchronized关键字可以使多个线程访问同一资源具有同步性,而且还具有将线程工作内存中的私有变量和公共内存中的变量同步的功能。