Java-多線程

一、基礎

1、臨界區:對資源的訪問順序敏感則存在競態條件,競態條件發生區叫臨界區,寫操作產生競態條件,需要同步。

2、死鎖:由於競爭資源或彼此通信導致阻塞,若無外力則無法推進,永遠在互相等待。屬於靜態的問題,死鎖發生進程被卡死,不會佔用cpu,它會被調出去,比較好發現和分析。

嵌套管程死鎖:線程1持有鎖A,同時等待從線程2發來的信號,但是線程2需要獲取鎖A才能給線程1發信號。

3、活鎖:兩線程一直謙讓,都無法使用資源,會比死鎖更難發現,因爲活鎖是一個動態的過程。

4、飢餓:線程無法獲得所需要的資源,導致一直無法執行。

阻塞:僅單線程使用;非阻塞:允許多線程同時進入臨界區。

1、無障礙:最弱的非阻塞,自由出入臨界區,無競爭時限定步驟內完成操作,有競爭則回滾數據,所有線程相當於拿到系統快照,直至拿到快照有效爲止。不斷嘗試導致線程相互干擾,卡死在臨區,不保證線程一定能完成。

2、無鎖:保證臨區有進有出,每次競爭有一個線程可以勝出,解決了無障礙的問題,保證了所有線程都順利執行下去,但是可能導致低優先級線程飢餓。

3、無等待:前提是無鎖,它保證所有的線程都必須在有限步內完成,消除飢餓的。

案例:

        1:如果只有讀線程,沒有寫線程,那麼這個則必然是無等待的;

        2:如果既有讀線程又有寫線程,而每個寫線程之前,都把數據拷貝一份副本,然後修改這個副本,而不是修改原始數據,因爲修改副本,則沒有衝突,那麼這個修改的過程也是無等待的。最後需要做同步的只是將寫完的數據覆蓋原始數據。

總結:無障礙->競爭回滾;無鎖->競爭一個線程勝出;無等待->限步,無飢餓。無鎖使用得更加廣泛一些。

二、Java的多線程

1、執行線程start方法後就會立即返回,不會等待到run方法執行完畢才返回。就好像run方法是在另外一個CPU上執行一樣。

2、Thread的子類可以執行多個實現了Runnable接口的線程,典型的應用就是線程池。

3、synchronized:在同步構造器(synchronized)中用括號括起來的對象叫做監視器對象

類的synchronized:static synchronized method(){...}和static method(){synchronized(MyClass.class)}效果等同。

對象的synchronized:實例方法和方法內synchronized(this)效果相同,使用了“this”,即爲調用同步方法的實例本身。

synchronized關鍵字並不是方法簽名的一部分。子類覆寫父類、接口的synchronized方法的時候,synchronized修飾符不會被自動繼承的。實例方法的同步在子類、父類中使用同樣的鎖。內部類方法的同步獨立於其外部類。非靜態的內部類方法可以鎖住其外部類。

4、線程已經啓動但是未終止,即使處於阻塞(Blocked),isAlive總是返回true。線程在開始運行前、結束運行後、被取消(cancelled)isAlive都會返回false,所以無法得知處於false的線程具體的狀態,即無法得知一個處於非活動狀態的線程是否已經被啓動過了。

5、線程無法得知自己是由哪個線程調用啓動的。

6、線程優先級:具有繼承性,比如A線程啓動B線程,則B線程的優先級與A是一樣的。

7、stop方法會清除棧內信息,結束該線程,丟棄所有的鎖,導致原子邏輯受損。

8、線程由native方法start0啓動,申請棧內存、運行run方法、修改線程狀態等職責,線程管理和棧內存管理都是由JVM負責的,如果覆蓋了start方法,也就是撤消了線程管理和棧內存管理的能力,所以不能複寫start()。

三、線程狀態分析:

wKiom1h8OPihMfvrAABCO9cnMKs624.png

每對象都只有一個 monitor,只能被一個線程擁有,該線程就是 “Active Thread”。而其它線程都是 “Waiting Thread”,分別在兩個隊列 “ Entry Set”和 “Wait Set”裏面等候。

“Entry Set”等待中的線程狀態: “Waiting for monitor entry”,

“Wait Set” 等待中的線程狀態: “in Object.wait()”。

當線程申請進入臨界區時,進入了 “Entry Set”隊列等待獲取monitor。

這時執行有以下可能性:

1、JVM檢查Entry Set裏面也沒有其它等待線程,說明鎖未被佔用,獲取monitor成功,執行臨界區的代碼,線程將處於 “Runnable”的狀態。

        只有一個- locked <0x00000007828050a0> (a java.io.BufferedInputStream)

2、獲取monitor失敗,進入Entry Set隊列中等待,DUMP中表現爲:“waiting for monitor entry” ,線程是阻塞狀態。

"Thread-1" prio=6 tid=0x000000000c1a0800 nid=0x2c78 waiting for monitor entry [0x000000000c9cf000]

 java.lang.Thread.State: BLOCKED (on object monitor)

          at java.lang.Object.wait(Native Method)
          - waiting on <0x0000000782804ce0> (a java.lang.Object)
          at thread.ThreadStatus$2.run(ThreadStatus.java:40)
          - locked <0x0000000782804ce0> (a java.lang.Object)

3、獲取monitor成功,但又調用了對象的 wait、join 方法,放棄了 monitor ,進入 “Wait Set”隊列。 DUMP中表現爲: in Object.wait()
join()方法實現是通過wait,當線程A調用線程B的Thread的join(N)方法時,首先得獲取到對象B的鎖,然後執行B的Object的wait(N)方法,直到B喚醒A,可以理解爲線程合併。核心邏輯 :while (isAlive()) {wait(N)}。

"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38]

java.lang.Thread.State: TIMED_WAITING (on object monitor)    對應wait(N)、join(N)

或者:java.lang.Thread.State: WAITING (on object monitor)    對應wait()、join()

at java.lang.Object.wait(Native Method)

waiting on <0xef63beb8> (a java.util.ArrayList)

at java.lang.Object.wait(Object.java:474)

locked <0xef63beb8> (a java.util.ArrayList)

at java.lang.Thread.run(Thread.java:595)

4、如果線程調用sleep(N),或者等待資源,狀態爲Wait on condition。

Wait on condition此時線程狀態大致爲以下幾種:

java.lang.Thread.State: TIMED_WAITING (sleeping)定時的;

java.lang.Thread.State: WAITING (parking):一直等那個條件發生;

java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定時的,那個條件不到來,也將定時喚醒自己。

dump對應的 parking to wait for <0x00000000acd84de8>

(at java.util.concurrent.SynchronousQueue$TransferStack)” 

首先,本線程肯定是在等待某個條件的發生,來把自己喚醒。其次,SynchronousQueue 並不是一個隊列,只是線程之間移交信息的機制,當我們把一個元素放入到 SynchronousQueue 中時必須有另一個線程正在等待接受移交的任務,其調度隊列用的是LinkedBlockingQueue, 執行take的時候會block住, 等待下一個任務進入隊列中, 然後進入執行,因此這就是本線程在等待的條件。

來源: http://www.cnblogs.com/zhengyun_ustc/archive/2013/03/18/tda.html

在 JDK 5.0中,引入了 Lock機制,從而使開發者能更靈活的開發高性能的併發多線程程序,可以替代以往 JDK中的 synchronized和 Monitor的 機制。但是,要注意的是,因爲 Lock類只是一個普通類, JVM無從得知 Lock對象的佔用情況,所以在線程 DUMP中,也不會包含關於 Lock的信息, 關於死鎖等問題,就不如用 synchronized的編程方式容易識別。

四、認識java線程安全,瞭解兩點:內存模型、線程同步機制。

 wKioL1h8OPrhm2qxAACG1Su18fo743.jpg


併發問題最終反映到java的內存模型上,要解決兩個主要的問題:可見性和有序性。
1、可見性: 多線程之間的通信只能通過共享變量來進行,不能互相傳遞數據。
每個線程在自己的工作內存存儲了主存對象的副本,當線程操作某個對象時,執行順序如下:
 (1) 從主存複製變量到當前工作內存 (read and load)
 (2) 執行代碼,改變共享變量值 (use and assign)
 (3) 用工作內存數據刷新主存相關內容 (store and write)
JVM規範定義了線程對主存的操作指令:readloaduseassignstorewrite
工作內存副本回寫主內存,其它線程可見,這就是多線程的可見性問題。
2、有序性:read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現系統決定。線程不能直接爲主存中中字段賦值,它會將值指定給工作內存中的變量副本(assign),完成後這個變量副本會同步到主存儲區(store-write),至於何時同步過去,根據JVM實現系統決定。

當同一線程多次重複對字段賦值時,比如:x=x+1

線程有可能只對工作內存中的副本進行賦值,只到最後一次賦值後才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。線程執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:
從主存中讀取變量x副本到工作內存
x1
x1後的值寫回主


synchronized解決執行有序性和內存可見性

如果調用obj.notify()則會通知阻塞隊列的某個線程進入就緒隊列。
每個鎖對象有
就緒、阻塞兩個隊列,就緒隊列存儲了將要獲得鎖(notify通知)的線程,阻塞隊列存儲了被阻塞的線程,JVM檢查鎖對象的就緒隊列有線程在等待,說明鎖被佔用。
一個線程執行臨界區代碼過程如下:
獲得同步鎖
清空工作內存
從主存拷貝變量副本到工作內存
對這些變量計算
將變量從工作內存寫回到主存
釋放鎖


volatile只保證內存可見,不能保證有序性,防止多線程下的指令重排序。
volatile和緩存一致性
1、 寫volatile 變量,JVM 就會向CPU發送一條 Lock 前綴的指令,將當前CPU緩存行的數據回寫到系統內存,此操作導致其他 CPU 裏緩存了該內存地址的數據無效。
volatile它所修飾的域的原子操作都不需要經過線程的工作內存,而直接在主內存中進行修改,volatile主要被用於變量只有原子操作的場合,如賦值、移位等。
2、緩存一致性機制:
多CPU下,爲了保證各個CPU的緩存是一致的,就會實現緩存一致性協議,每個CPU通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當CPU發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當CPU要對這個數據進行修改操作的時候,會強制重新從系統內存裏把數據讀到處理器緩存裏。
阻止同時修改被多個CPU緩存的內存區域數據,一個CPU的緩存回寫到內存會導致其他CPU的緩存無效。
3、不要將數組成員聲明爲volatile類型的。如果鎖住了一個數組並不代表其數組成員都可以被原子的鎖定。也沒有能在一個原子操作中鎖住多個對象的方法。

要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

1)對變量的寫操作不依賴於當前值。
2)該變量沒有包含在具有其他變量的不變式中。


線程的sleep()方法和yield()方法有什麼區別? 異常拋出:sleep拋InterruptedException,yield無

優先級考慮:sleep無,yield相同或更高

線程狀態:sleep進入阻塞,yield進入就緒,僅建議JVM對其它就緒狀態的線程調度執行,而當前線程放棄時間不確定,有可能剛放棄,有馬上獲得CPU

可移植性:sleep>yield,與CPU調度相關

一個還沒有啓動的線程上調用join方法是沒有任何意義。

1、wait、join(內部實現wait)、sleep都涉及到了線程的中斷,必須捕獲InterruptedException,notify和notifyAll不需要捕獲異常。

2、wait,notify和notifyAll是Object的實例方法,用於線程間通信,貢獻對象,需要在同步機制,可用於不同線程間的調度,並且只能在其他線程調用本實例的notify()或者notifyAll()方法時被喚醒。

3、wait後進入等待鎖定池,只有針對此對象發出notify或者notifyAll方法後,獲得對象鎖進入就緒狀態,等到CPU調度

4、Thread.sleep是一個靜態方法,如果線程A調用線程B的sleep()的時候,則線程A進入休眠狀態,不會暫停線程B。


interrupt:

默認情況下,新建線程和創建它的線程屬於同一個線程組,可以獲取同一個線程組的其他線程的標識。當創建一個新的線程組,這個線程組成爲當前線程組的子組,對不屬於同一組的線程調用interrupt是不合法的。

interrupt方法不能終止一個線程狀態,它只會改變中斷標誌位(如果在t1.interrupt()前後輸出t1.isInterrupted()則會發現分別輸出了false和true)。

ThreadGroup類uncaughtException,當線程組中的某個線程因拋出未檢測的異常(比如空指針異常NullPointerException)而中斷的時候,調用這個方法可以打印出線程的調用棧信息。

一個線程的中斷狀態是不允許被其他線程清除的。

2、如果Thread實例A、B,A線程調用B的interrupt方法。如果B正在wait/sleep/join(如果在執行普通的代碼,不拋出中斷異常),則線程B會立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結束線程。

但是InterruptedException是線程自己從內部拋出的,並不是interrupt()方法拋出的。


happen before原則:

一個監視器鎖的unlock happen before 之後每一個對該監視器的lock

一個volatile字段 寫操作 happen before 之後的每一個讀

一個線程的start操作happen-before 線程內的任何操作

線程內的任何操作都happen-before任何從該線程的join()方法返回的


JVM與多線程:

虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性

如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操作序列的外部, 在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標誌位爲“01”狀態),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,然後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位(Mark Word的最後2bit)將轉變爲“00”,即表示此對象處於輕量級鎖定狀態,如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果只說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖對象已經被其他線程搶佔了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 上面描述的是輕量級鎖的加鎖過程,它的解鎖過程也是通過CAS操作來進行的,如果對象的Mark Word仍然指向着線程的鎖記錄,那就用CAS操作把對象當前的Mark Word和線程中複製的Displaced Mark Word替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要在釋放鎖的同時,喚醒被掛起的線程。 輕量級鎖能提升程序同步性能的依據是“對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗數據。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。 如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連CAS操作都不做了。 偏向於第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。 當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位爲“01”)或輕量級鎖定(標誌位爲“00”)的狀態。


多線程的弊端:

在Linux這樣的操作系統中,線程本質上就是一個進程。創建和銷燬都是重量級的系統函數。

像Java的線程棧,一般至少分配512K~1M的空間,超過1000線程,內存佔用過高。

如果線程數過高,可能執行線程切換的時間甚至會大於線程執行的時間,這時候帶來的表現往往是系統load偏高、CPU sy使用率特別高。


sleep是Thread類的靜態方法,這意味着只對當前線程有效,sleep多長時間是由當前線程決定,sleep將在接到時間到達事件事恢復線程執行,如果時間不到你只能調用interreput()來強行打斷進行喚醒。

wait是Object的實例方法,也就是說可以對任意一個對象調用wait方法,調用wait方法將會將調用者的線程掛起。

本質的區別是sleep:一個線程的運行狀態控制,wait是線程之間的通訊的問題。

wait():如果當前線程不是對象所得持有者,該方法拋出一個java.lang.IllegalMonitorStateException 異常

notifyAll();相當於this.notifyAll();

同理wait();相當於this.wait();

注意java.lang.IllegalMonitorStateException 異常


若不擁有對象的鎖標記,而試圖用wait/notify協調共享對象資源,應用程序將拋出 IllegalMonitorStateException 


IllegalMonitorStateException 意味着一個或多個資源可能不再處於一致狀態,表示程序出現了嚴重問題。由於IllegalMonitorStateException是RuntimeException類型,因此它可能中斷產生異常的線程。


當且僅當創建線程是守護線程時,新線程纔是守護程序


在Spring中,DAO和Service都以單實例的方式存在。Spring是通過ThreadLocal將有狀態的變量(如Connection 等)本地線程化,達到另一個層面上的“線程無關”,從而實現線程安全。Spring不遺餘力地將有狀態的對象無狀態化,就是要達到單實例化Bean的目 的。

當DAO類作爲一個單例類時,數據庫鏈接(connection)被每一個線程獨立的維護,互不影響。(基於線程的單例)

我們可以得出這樣的結論:在相同線程中進行相互嵌套調用的事務方法工作於相同的事務中。如果這些相互嵌套調用的方法工作在不同的線程中,則不同線程下的事務方法工作在獨立的事務中。


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