【Java多線程】——volatile關鍵字

    首先我們來看一個死循環的問題:

public class RunThread extends Thread{
    private boolean isRunning = true;
	public boolean isRunning() {
		return isRunning;
	}
	
	public void setRunning(boolean isRunning) {
		this.isRunning = isRunning;
	}
	@Override
	public void run() {
		System.out.println("進入run");
		while(isRunning == true) {
		}
		System.out.println("線程停止!");
	}
}
public class Run {
	public static void main(String args[]) {
		try {
			RunThread thread = new RunThread();
			thread.start();
			Thread.sleep(1000);
			thread.setRunning(false);
			System.out.println("賦值爲false");
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

運行上述程序,可以發現,本應輸出“線程停止”這句話,但是實際的輸出是如下:

/*

 

進入run

賦值爲false

*/

一、解決不可見問題

    出現這種情況的原因是,雖然程序執行了setRunning方法將變量設置爲false,但是,這個false數據被存在了公共堆棧中,而線程在工作的時候,對於數據的操作一般不直接存入公共內存,而是先存放在緩存中或者說是線程的私有堆棧中。這就導致了不可見的問題,也就是說雖然公共堆棧中的isRunning變量已經變爲了false,但是由於線程的私有堆棧中的值依舊爲true,所以線程會繼續執行while循環而陷入無限等待。

    這就涉及到了一個可見性的問題,對於可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。而普通的共享變量不能保證可見性,因爲普通共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。

    另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

二、非原子性

    volatile雖然可以解決不可見的問題,但是不支持原子性,下面將volatile與synchronized做一下比較:

1)volatile是線程同步的輕量級實現,它的效率要比synchronized好,並且volatile只能修飾變量而不能修飾方法、代碼塊。

2)多線程訪問volatile不會發生阻塞,而訪問synchronized則會發生阻塞。

3)voaltile保證數據可見性但不保證數據的原子性,synchronized可以保證原子性也可以間接保證可見性,因爲它會將私有內存和公共內存中的數據同步。

4)再次劃重點,volatile解決的是多個變量在線程之間的可見性,而synchronized解決的是多個線程訪問一個互斥資源時的同步性。

    volatile關鍵字的非原子性操作體現在,假設多個線程共同進行一個變量的自增操作,每個線程增加10次,一共有10個線程共同完成從0到100的增加,將一個int型的變量i用volatile修飾,分別由這10個線程完成自增。但是由於i++或者是i=i+1這樣的操作不是原子操作(這裏解釋一下,i++;這個操作實際上是三次操作,首先取出i的值,然後+1,最後將結果存入內存),想象一下,如果在一個線程取出i並+1時,還沒有存入內存,另一個線程從內存中取出了原來的i值也+1,然後這兩個任務各自將+1後的i存入了內存中,內存中的i實際上值增加了1,但是原本應該增加2,這就是髒讀的一種。雖然volatile保證了每次取出的i都是從內存中取出的,保證了可見性,但是取不能保證操作的原子性。這時,我們可以使用synchronized代碼塊來限制多個線程對於變量i的訪問,從而保證原子性。

三、原子類

    這裏額外介紹一下Java中的原子類AtomicInteger,原子類中的一系列方法比如自增,加一個數並返回結果等等操作都是保證原子性的,原子類提供了以下方法:

 //獲取當前的值
 
 public final int get()
 
 //取當前的值,並設置新的值
 
  public final int getAndSet(int newValue)
 
 //獲取當前的值,並自增
 
  public final int getAndIncrement() 
 
 //獲取當前的值,並自減
 
 public final int getAndDecrement()
 
 //獲取當前的值,並加上預期的值
 

 public final int getAndAdd(int delta)

 

PS:  可以用synchronized關鍵字實現volatile的功能,因爲synchronized會使線程在釋放鎖之前將對變量所做的改變刷新到主存中,從而保證可見性。於是,我們只要將上面的代碼改爲

public void run() {
		String str = new String();
		System.out.println("進入run");
		while(isRunning == true) {
			synchronized(str) {
			}
		}
		System.out.println("線程停止!");
	}

那麼由於不可見性的原因造成的死循環就解決了。

發佈了52 篇原創文章 · 獲贊 13 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章