?
1、對象的狀態:對象的狀態是指存儲在狀態變量中的數據,對象的狀態可能包括其他依賴對象的域。在對象的狀態中包含了任何可能影響其外部可見行為的數據。
?
2、一個對象是否是線程安全的,取決于它是否被多個線程訪問。這指的是在程序中訪問對象的方式,而不是對象要實現的功能。當多個線程訪問某個狀態變量并且其中有一個線程執行寫入操作時,必須采用同步機制來協同這些線程對變量的訪問。同步機制包括synchronized、volatile變量、顯式鎖、原子變量。
?
3、有三種方式可以修復線程安全問題:
1)不在線程之間共享該狀態變量
2)將狀態變量修改為不可變的變量
3)在訪問狀態變量時使用同步
?
4、線程安全性的定義:當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,并且在主調代碼中不需要任何額外的同步,這個類都能表現出正確的行為,那么就稱這個類是線程安全的。
?
5、無狀態變量一定是線程安全的,比如局部變量。
?
6、讀取-修改-寫入操作序列,如果是后續操作是依賴于之前讀取的值,那么這個序列必須是串行執行的。在并發編程中,由于不恰當的執行時序而出現不正確的結果是一種非常重要的情況,它稱為競態條件(Race Condition)。最常見的競態條件類型就是先檢查后執行的操作,通過一個可能失效的觀測結果來決定下一步的操作。
7、復合操作:要避免競態條件問題,就必須在某個線程修改該變量時,通過某種方式防止其他線程使用這個變量,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態,而不是在修改狀態的過程中。假定有兩個操作A和B,如果從執行A的線程看,當另一個線程執行B時,要么將B全部執行完,要么完全不執行B,那么A和B對彼此來說就是原子的。原子操作是指,對于訪問同一個狀態的所有操作來說,這個操作是一個以原子方式執行的操作。
為了確保線程安全性,讀取-修改-寫入序列必須是原子的,將其稱為復合操作。復合操作包含了一組必須以原子方式執行的接口以確保線程安全性。
?
8、在無狀態的類中添加一個狀態時,如果這個狀態完全由線程安全的對象來管理,那么這個類仍然是線程安全的。(比如原子變量)
?
9、如果多個狀態是相關的,需要同時被修改,那么對多個狀態的操作必須是串行的,需要進行同步。要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量。
?
10、內置鎖:synchronized(object){同步塊}
Java的內置鎖相當于一種互斥體,這意味著最多只有一個線程能持有這種鎖,當線程A嘗試獲取一個由線程B持有的鎖時,線程A必須等待或阻塞,直到線程B釋放這個鎖。如果B永遠不釋放鎖,那么A也將永遠地等待下去。
?
11、重入:當某個線程請求一個由其他線程持有的鎖時,發出請求的線程就會阻塞。然而,由于內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由它自己持有的鎖,那么這個請求就會成功。重入意味著獲取鎖的操作的粒度是線程,而不是調用。重入的一種實現方法是,為每個鎖關聯一個獲取計數值和一個所有者線程。當計數值為0時,這個鎖就被認為是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,并且將獲取計數值置1。如果一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數值會相應遞減。當計數值為0時,這個鎖將被釋放。
?
12、對于可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變量是由這個鎖保護的。
每個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員知道是哪一個鎖。
一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,并提供對象的內置鎖(this)對所有訪問可變狀態的代碼路徑進行同步。在這種情況下,對象狀態中的所有變量都由對象的內置鎖保護起來。
?
13、不良并發:要保證同步代碼塊不能過小,并且不要將本應是原子的操作拆分到多個同步代碼塊中。應該盡量將不影響共享狀態且執行時間較長的操作從同步代碼塊中分離出去,從而在這些操作的執行過程中,其他線程可以訪問共享狀態。
?
14、可見性:為了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
?
15、加鎖與可見性:當線程B執行由鎖保護的同步代碼塊時,可以看到線程A之前在同一個同步代碼塊中的所有操作結果。如果沒有同步,那么就無法實現上述保證。加鎖的含義不僅僅局限于互斥行為,還包括內存可見性。為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或寫操作的線程都必須在同一個鎖上同步。
?
16、volatile變量:當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或其他對處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。volatile的語義不足以確保遞增操作的原子性,除非你能確保只有一個線程對變量執行寫操作。原子變量提供了“讀-改-寫”的原子操作,并且常常用做一種更好的volatile變量。
?
17、加鎖機制既可以確保可見性,又可以確保原子性,而volatile變量只能確保可見性。
?
18、當且僅當滿足以下的所有條件時,才應該使用volatile變量:
1)對變量的寫入操作不依賴變量的當前值(不存在讀取-判斷-寫入序列),或者你能確保只有單個線程更新變量的值。
2)該變量不會與其他狀態變量一起納入不可變條件中
3)在訪問變量時不需要加鎖
?
19、棧封閉:在棧封閉中,只能通過局部變量才能訪問對象。維護線程封閉性的一種更規范的方法是使用ThreadLocal,這個類能使線程的某個值與保存值的對象關聯起來,ThreadLocal通過了get和set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。
?
20、在并發程序中使用和共享對象時,可以使用一些使用的策略,包括:
1)線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只能由這個線程修改。
2)只讀共享:在沒有額外同步的情況下,共享的只讀對象可以由多個線程并發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象(從技術上來說是可變的,但其狀態在發布之后不會再改變)。
3)線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步。
4)保護對象。被保護的對象只能通過持有對象的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發布并且由某個特定鎖保護的對象。
?
21、饑餓:當線程由于無法訪問它所需要的資源而不能繼續執行時,就發生了饑餓(某線程永遠等待)。引發饑餓的最常見資源就是CPU時鐘周期。比如線程的優先級問題。在Thread API中定義的線程優先級只是作為線程調度的參考。在Thread API中定義了10個優先級,JVM根據需要將它們映射到操作系統的調度優先級。這種映射是與特定平臺相關的,因此在某個操作系統中兩個不同的Java優先級可能被映射到同一優先級,而在另一個操作系統中則可能被映射到另一個不同的優先級。
當提高某個線程的優先級時,可能不會起到任何作用,或者也可能使得某個線程的調度優先級高于其他線程,從而導致饑餓。
通常,我們盡量不要改變線程的優先級,只要改變了線程的優先級,程序的行為就將與平臺相關,并且會導致發生饑餓問題的風險。
?
事務T1封鎖了數據R,事務T2又請求封鎖R,于是T2等待。T3也請求封鎖R,當T1釋放了R上的封鎖后,系統首先批準了T3的請求,T2仍然等待。然后T4又請求封鎖R,當T3釋放了R上的封鎖之后,系統又批準了T的請求......T2可能永遠等待
?
22、活鎖
活鎖是另一種形式的活躍性問題,該問題盡管不會阻塞線程,但也不能繼續執行,因為線程將不斷重復執行相同的操作,而且總會失敗。活鎖通常發生在處理事務消息的應用程序中。如果不能成功處理某個消息,那么消息處理機制將回滾整個事務,并將它重新放到隊列的開頭。雖然處理消息的線程并沒有阻塞,但也無法繼續執行下去。這種形式的活鎖通常是由過度的錯誤恢復代碼造成的,因為它錯誤地將不可修復的錯誤作為可修復的錯誤。
?
當多個相互協作的線程都對彼此進行響從而修改各自的狀態,并使得任何一個線程都無法繼續執行時,就發生了活鎖。要解決這種活鎖問題,需要在重試機制中引入隨機性。在并發應用程序中,通過等待隨機長度的時間和回退可以有效地避免活鎖的發生。
?
23、當在鎖上發生競爭時,競爭失敗的線程肯定會阻塞。JVM在實現阻塞行為時,可以采用自旋等待(Spin-Waiting,指通過循環不斷地嘗試獲取鎖,直到成功),或者通過操作系統掛起被阻塞的線程。這兩種方式的效率高低,取決于上下文切換的開銷以及在成功獲取鎖之前需要等待的時間。如果等待時間較短,則適合采用自旋等待的方式,而如果等待時間較長,則適合采用線程掛起方式。
?
24、有兩個因素將影響在鎖上發生競爭的可能性:鎖的請求頻率,以及每次持有該鎖的時間。如果二者的乘積很小,那么大多數獲取鎖的操作都不會發生競爭,會因此在該鎖上的競爭不會對可伸縮性造成嚴重影響。然而,如果在鎖上的請求量很高,那么需要獲取該鎖的線程將被阻塞并等待。在極端情況下,即使仍有大量工作等待完成,處理器也會被閑置。
有3種方式可以降低鎖的競爭程度:
1)減少鎖的持有時間:
①縮小鎖的范圍,將與鎖無關的代碼移出同步代碼塊,尤其是開銷較大的操作以及可能被阻塞的操作(IO操作)。
當把一個同步代碼塊分解為多個同步代碼塊時,反而會對性能提升產生負面影響。在分解同步代碼塊時,理想的平衡點將與平臺相關,但在實際情況中,僅可以將一些大量的計算或阻塞操作從同步代碼塊移出時,才應該考慮同步代碼塊的大小。
②減小鎖的粒度:鎖分解和鎖分段
鎖分解是采用多個相互獨立的鎖來保護獨立的狀態變量,從而改變這些變量在之前由單個鎖來保護的情況。這些技術能減小鎖操作的粒度,并能實現更高的可伸縮性,然而,使用的鎖越多,那么發生死鎖的風險也就越高。
鎖分段:比如JDK1.7及之前的ConcurrentHashMap采用的方式就是分段鎖的方式。
2)降低鎖的請求頻率
3)使用帶有協調機制的獨占鎖,這些機制允許更高的并發性
比如讀寫鎖,并發容器等
?