关于Java变量的可见性以及时间片切换的总结

时间:2022-01-29 21:13:50

前言:今天被一个同学问到一个很有意思的题目,在这个看起来很简单的程序中却涉及了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,所以如果主线程执行的代码不多的话,子线程将在主线程执行完之后才执行。

结束:这是本人结合网上资料和个人理解的整理,如有错误,欢迎指正,拒绝人身攻击。