前言:今天被一个同学问到一个很有意思的题目,在这个看起来很简单的程序中却涉及了Java变量可见性和线程之间时间片切换的知识,感觉很典型,所以写这篇文章总结一下。
帮助文档连接:http://www.importnew.com/19434.html
package code; public class good extends Thread{ private static boolean flag = false; public void run(){ while(!flag){ 1 // System.out.println(); } } public static void main(String[] args) throws Exception{ new good().start(); Thread.sleep(100); flag = true; } }
看以上代码,通过编译运行你会发现,如果将System.out.println()这条语句注释掉,程序无法终止;而有这条语句时程序又可以正常终止。是不是感觉很困惑?没关系,我刚开始也是如此。要理解这个问题就不得不谈谈java变量的可见性了。
Java变量的可见性
首先了解一下涉及的三个关键字volatile、synchronized、sleep。
volatile:此关键字保证了变量在线程的可见性,所有线程访问由volitale修饰的变量,都必须从主存中读取后操作,并在工作内存修改后立即写回主存,保证了其他线程的可见性,同样效果的关键字还有final。
synchronized:所有同步操作都必须保证1、原子性 2、可见性,所以在同步快中发生的变化回立马写回主存。
sleep:此方法只会让出CPU执行时间,并不会释放锁。
问题1:为什么上面没有System.out.println()语句后程序不会终止?
回答1:因为执行start()方法开启子线程之后,主线程继续执行sleep()语句,这时CPU会被让出从而执行子线程,子线程就会把当前boolean flag = false的值加载到自己的工作内存中,然后当主线程的休眠时间结束之后,CPU会等子线程执行完当前时间片便切换回主线程继续执行flag = true,并且会立马将flag的值写回主内存中,由于子线程取得是主线程修改之前主内存中的flag值,所以程序会陷入无限循环中,无法终止。
问题2:为什么有System.out.println()语句之后程序会终止?
package code; public class good extends Thread{ private static boolean flag = false; private static int i=0; public void run(){ while(!flag){ 1 // Object[] a=new Object[10000]; 2 // synchronized(this){} 3 // System.out.println(); // try { 4 // Thread.sleep(100); // } catch (InterruptedException e) { // // TODO Auto-generated catch block // e.printStackTrace(); // } } } public static void main(String[] args) throws Exception{ new good().start(); Thread.sleep(100); flag = true; } }
回答2:看如上代码,不仅仅是System.out.println()语句,代码块(1,2,4)这三种方式也可以终止程序。为什么呢?首先我们需要知道,JVM针对现在的硬件水平已经做了很大程度的优化,基本上很大程度的保障了工作内存和主内存的及时同步,相当于默认使用了volitale。但只是最大程度!在CPU资源一直被占用的时候,工作内存与主内存中间的同步,也就是变量的可见性就不会那么及时!现在,我们回过头来分析为什么(1,2,3,4)代码块会更新线程栈中的flag变量值呢?其实就是我们刚刚讲的CPU空闲后会遵循JVM优化基准,尽可能快的保证数据的可见性,从而从主存同步flag变量到工作内存,最终导致程序结束。然后肯定会有人问为什么这4个代码块会产生CPU空闲呢?sleep关键字前面就说了,它不会释放锁但是它会释放CPU。而new Object[10000]存在大量的内存分配,因为CPU的处理速度明显快过内存,不然也不会有CPU的寄存器,所以大量的耗时都花在了内存的分配上,CPU仍然有空闲。而(2,3)代码块有一个共同特点,就是都涉及到了synchronized同步锁,尽管前面讲了synchronized只会保证在同步块中的变量的可见性,但是别忘了锁同步是个很耗时的操作,所以同步过程时CPU也能有空闲。
时间片切换
为什么要提时间片切换呢?感觉这里跟时间片切换也没什么关系啊?这是因为刚开始我就把程序不终止的原因理解错了,我以为Thread.sleep(100)是为了让主线程占有的当前时间片处于空闲中,然后等到下一个时间片去执行子线程。所以我还尝试了将Thread.sleep(1),但是还是不终止,最后才知道是空闲CPU会优化变量可见性的原因。尽管开始理解错了,但是我认为这也是一种存在的情况,你比如下面代码:
package code; public class good extends Thread{ private static boolean flag = false; private static int i=0; public void run(){ while(!flag){ // } } public static void main(String[] args) throws Exception{ new good().start(); for(int i=0;i<10000;i++) System.out.println(); flag = true; } }
这就是典型时间片切换导致的无法终止。这里必须明白:尽管主线程和子线程的优先级是相同的,但是主线程占用着CPU,所以如果主线程执行的代码不多的话,子线程将在主线程执行完之后才执行。
结束:这是本人结合网上资料和个人理解的整理,如有错误,欢迎指正,拒绝人身攻击。