首先我們來看一個死循環的問題:
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("線程停止!");
}
那麼由於不可見性的原因造成的死循環就解決了。