天天看點

[Java基礎] Java線程複習筆記

先說說線程和程序,現代作業系統幾乎無一例外地采用程序的概念,程序之間基本上可以認為是互相獨立的,共享的資源非常少。線程可以認為是輕量級的進 程,充分地利用線程可以使得同一個程序中執行多種任務。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基礎] Java線程複習筆記

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()方法):

死鎖(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對象

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

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