Java線程複習筆記

最近有騎驢找馬的打算,咱們這行工作和麪試其實差距很大(其他行業可能差的更大),就拿線程來說吧,平時工作中大部分時候是不需要管這玩意兒的,除非真遇到瓶頸了或者performance issue了,但是參加面試卻幾乎必問,沒辦法,只好複習一些基本的知識,以免栽在簡單的問題上。


先說說線程和進程,現代操作系統幾乎無一例外地採用進程的概念,進程之間基本上可以認爲是相互獨立的,共享的資源非常少。線程可以認爲是輕量級的進程,充分地利用線程可以使得同一個進程中執行多種任務。Java是第一個在語言層面就支持線程操作的主流編程語言。和進程類似,線程也是各自獨立的,有自己的棧,自己的局部變量,自己的程序執行並行路徑,但線程的獨立性又沒有進程那麼強,它們共享內存,文件資源,以及其他進程層面的狀態等。同一個進程內的多個線程共享同樣的內存空間,這也就意味着這些線程可以訪問同樣的變量和對象,從同一個堆上分配對象。顯然,好處是多線程之間可以有效共享很多資源,壞處是要確保不同線程之間不會產生衝突。


每個Java程序都至少有一個線程——main線程。當Java程序開始運行時,JVM就會創建一個main線程,然後在這個main線程裏面調用程序的main()方法。JVM同時也會創建一些我們看不到的線程,比如用來做垃圾收集和對象終結的(garbage collection and object finalization,JVM最重要的兩種資源回收),或者JVM層面的其他整理工作。


爲什麼要使用線程?

1、可以使UI(用戶界面)更有效(利用多線程技術,可以把時間較長的UI工作交給專門的線程,這樣UI的主線程就不會被長期佔用,界面就會流暢而不停滯)

2、有效利用多進程系統(單線程+多進程,太浪費系統資源了)

3、簡化建模

4、執行異步處理或者後臺處理(不同的線程做不同的工作)


線程的生命週期:

通常有兩種方法創建一個線程,1、implement Runnable接口,2、繼承Thread類

創建完成後,這個線程就進入了New State,直到它的start()方法被調用,它就進入了Runnable狀態。

一個線程從Running State進入Terminated / Dead State標誌着線程的終結,正常情況下有這麼幾種可能性:

1、線程的run()執行結束

2、線程拋出沒有捕捉到的異常或者錯誤

當一個Java程序所有的非守護進程(Daemon Thread,即守護進程,負責一些包括資源回收在內的任務,我們無法結束這些進程)結束時,程序宣告執行結束。



Java Thread的重要方法必須熟悉。

join():目標線程結束之前調用線程將會被Block,例如在main線程中創建了一個thread1線程,調用thread1.join(),這就意味着thread1將優先執行,在thread1結束後main thread纔會繼續。一個join()方法的使用案例:將一個任務(比如從1萬個元素的數組中選出最大值)分拆成10個小任務(每個小任務負責1000個)分配給10個線程,調用它們的start(),然後分別調用join(),以確保10個任務都完成(分別選出了各自負責的1000個元素中的最大值)後,主任務再進行下去(從10個結果中挑出最大值)。

sleep():使當前線程進入Waiting State,直到指定的時間到了,或者被其他線程打斷,從而回到Runnable State。

wait():使調用線程進入Waiting State,直到被打斷,或者時間到,或者被其他線程使用notify(),notifyAll()叫醒。

wait和sleep有一個非常重要的區別是,一個線程sleep的時候不會釋放任何lock,而wait的時候會釋放該對象上的lock。

notify():這個方法被一個對象調用時,如果有多個線程在等待這個對象,這些處於Waiting State的線程中的一個會被叫醒。

notifyAll():這個方法被一個對象調用時,如果有多個線程在等待這個對象,這些處於Waiting State的線程都會被叫醒。


多線程共享資源是討論最多的話題,也是最容易出問題的地方之一,Java定義了兩個關鍵字,synchronized和volatile,用來幫助共享的變量在多線程情況下能夠正常工作。

synchronized一方面確保同一時間內只有一個線程能夠執行一段受保護的代碼,並且這個線程對數據(變量)進行的改動對於其他線程是可見的。這裏包含兩層意思:前者依靠lock(鎖)來實現,當一個線程處理一段受保護代碼時,該線程就擁有lock,只有它釋放了這個lock,其他線程纔有可能獲得並訪問這段代碼;後者由JVM機制實現,對於受synchronized保護的變量,需要讀取時(包括獲取lock)會首先廢棄緩存(invalidate cache),進而直接讀取main memory上的變量,完成改動時(包括釋放lock)會flush緩存中的write operation,強行把所有改動更新到main memory。

爲了提高performance,處理器都是會利用緩存來保存一些變量儲存在內存中的地址,這樣就存在一種可能性,在一個多進程架構中,一個內存地址在一個進程的緩存中被修改了,其他進程並不會自動獲得更新,於是不同進程上的2個線程就會看到同一個內存變量的兩個不同值(因爲兩個緩存中的保存的內存地址不同,一個被修改過)。Volatile關鍵字可以有效地控制原始類型變量(primitive variable,比如integer,boolean)的單一實例:當一個變量被定義爲volatile的時候,無論讀寫,都會繞過緩存而直接對main memory進行操作。


關於Java的鎖(Locking)有一個問題需要注意:一段被lock保護的代碼並不意味着就一定不能被多線程同時訪問,而只意味着不能被等待同一個lock的多線程同時訪問。

對於絕大多數的synchronized方法,它的lock就是調用方法的實例對象;對於static synchronized方法,它的lock是定義方法的類(因爲static方法是每個類只有一份copy,而不是每個實例都有一份copy)。因此,即使一個方法被synchronized保護了,多線程仍然可以同時調用這個方法,只要它們是調用不同實例上的這個方法。

synchronized代碼塊稍微複雜一些,一方面它也需要和synchronized方法一樣定義lock的類型,另一方面必須考慮如果最小化被保護的代碼塊,即能不放到synchronized裏面就不放進去,比如局部變量的訪問通通不需要保護,因爲局部變量本身就只存在於單線程上。

下面兩種加鎖的方法是等效的,都是以Point類的實例爲lock(即多線程可以同時訪問不同Point實例的synchronized setXY()方法):

public class Point {
  public synchronized void setXY(int x, int y) {
this.x = x;
this.y = y; }
}

public class Point {
  public void setXY(int x, int y) {
    synchronized (this) {
      this.x = x;
      this.y = y;
} }
}

死鎖(deadlock)是多線程編程中最怕遇到的情況。什麼是死鎖?當2個或2個以上的線程因爲等待彼此釋放lock而處於無限的等待狀態就稱爲死鎖。簡單來說就是線程1擁有對象A的lock,等待獲取對象B的lock,線程2擁有對象B的lock,等待獲取對象A的lock,這樣就沒完沒了了。

如何檢測deadlock?

檢查代碼,看是否有層疊的synchronized代碼塊,或者調用彼此的synchronized方法,或者試圖獲取多個對象上的lock,等等。如果程序員不注意的話,這些情況都容易導致deadlock。

怎麼防止deadlock是一個大話題,可以寫一本書,簡單來說的話就是當線程需要獲取多個lock的時候(比如線程1和2都要獲取對象A和B的lock),永遠按照一定的次序來。比如如果線程1和2都是先獲取對象A的lock,再獲取對象B,那就不會出現上面的deadlock了,因爲如果1獲得了A lock,2就得等,而不是去獲得B lock。


總結一下synchronized關鍵字的一些注意點:

1、synchronized關鍵字確保了需要同一個lock的多線程永遠無法同時或並行訪問同一個共享資源或者synchronized方法

2、synchronized關鍵字只能修飾方法或者代碼塊

3、任何時候一個線程想要訪問synchronized方法或者代碼塊時,都要先獲取lock,任何時候一個線程結束訪問synchronized方法或代碼塊時,都會釋放lock。即使因爲錯誤或異常結束訪問,也會釋放lock

4、Java線程進入一個實例層synchronized方法時,要先獲取對象層面的lock(object level lock);進入靜態synchronized方法時,要先獲取類層面的lock(class level lock)

5、一個Java synchronized方法調用另一個synchronized方法,兩個方法需要同一個lock的時候,線程不需要重新獲取lock

6、在synchronized(myInstance)中,如果myInstance爲Null,會拋出NullPointerException

7、synchronized關鍵字一個主要缺點就是它不支持並行的讀取(因此對於一些值不可變的情況不要使用這個關鍵字,否則會無謂地影響performance)

8、synchronized關鍵字還有一個限制,它只支持單一JVM內的共享資源訪問,對於多JVM共享一些文件資源或者數據庫資源的時候,單單使用它就不夠了,這時候程序員需要實現全局性的lock

9、synchronized關鍵字對performance影響很大,因此只有當真正需要的時候才用

10、優先使用synchronized代碼塊,而不是synchronized方法,確保將synchronized代碼減小到最精,能不synchronized就不用synchronized關鍵字

11、靜態和非靜態的synchronized方法可能同時或者並行運行,因爲它們被認爲是使用了不同的lock(一個是object level,一個是class level)

12、從Java 5開始,對於volatile修飾的變量,讀和寫都被保證是原子的(atomic),即安全的。從performance的角度,操作volatile變量比從synchronized代碼中訪問變量要高效

13、synchronized代碼可能會導致死鎖

14、Java不允許在構造函數中使用synchronized關鍵字。理由很簡單,如果構造函數中出現synchronized關鍵字,那當一個線程在構造實例時,其他線程都不知道,這就違背了同步的原則

15、synchronized關鍵字不能用於修飾變量,正如volatile關鍵字不能用於修飾方法

16、Java.util.concurrent.locks包提供了synchronized關鍵字的擴展功能,可以幫助程序員編寫更爲複雜的多線程操作

17、synchronized關鍵字同步內存(線程內存和主內存)

18、Java synchronization的一些關鍵方法,比如wait()、notify()、notifyAll(),定義在Object類中

19、在synchronized代碼塊中不要以非final變量(non final field)爲鎖,因爲非final變量的引用常常會改變,一旦鎖改變了,那synchronization就失去了意義。比如這個例子,一旦對String變量進行操作,就在內存中生成新的String對象

private String lock = new String("lock");
synchronized(lock){
System.out.println("locking on :"  + lock);
}

20、不推薦使用String對象作爲synchronized代碼塊的鎖,即使是final String。因爲String存放在內存的String變量池中,可能會有其他代碼或者第三方的代碼使用了同一個String對象爲鎖,這樣容易導致一些無法預測的問題。在下面的例子中,與其使用LOCK爲鎖,還不如創建一個Object實例爲鎖。

private static final String LOCK = "lock";   //not recommended
private static final Object OBJ_LOCK = new Object(); //better

public void process() {
   synchronized(LOCK) {
      ........
   }
}

21、在Java庫中,很多類默認不是線程安全的,需要程序員特別注意加上安全保護,比如Calendar, SimpleDateFormat等。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章