存線上程安全問題的前提:
該變數可被修改
該變數被多個執行緒存取
當多個執行緒存取同一個可變狀態的變數時,沒有進行合適的同步,那麼程式就會出現錯誤。共有三種方法解決該問題:
執行緒安全性:當多個執行緒存取某個類時,這個類始都能出現正確的行為,那麼我們就稱這個類是執行緒安全的。
無狀態的物件一定是執行緒安全的
(有狀態與無狀態:有狀態就是有儲存資料的功能的類。無狀態就是該類中的屬性不儲存資料)
1.競態條件: 當多個執行緒存取同一資源,執行緒執行的先後順序不同會造成返回結果不一致的結果,此時就發生了競態現象。多個執行緒存取的程式碼資源,叫做臨界區。
2.延遲初始化中的競態條件: 延遲初始化,會在物件被需要的時候才進行初始化操作,以此來節約記憶體,同時我們必須確保物件只被初始化一次。但是在多執行緒情況下,兩個執行緒同時執行初始化條件,當第一個執行緒初始化未完成時,第二個執行緒如果在此時進行條件判斷。則得到的結果則是該物件還未被初始化,那麼此時第二個執行緒會再次執行初始化。此時即發生了執行緒安全問題。本該執行一次的程式碼,被兩個執行緒分別執行了一次。如果在1號與2號執行緒初始化完成前,又有執行緒對初始化條件進行了判斷。則又會多執行一次初始化。這是我們不願意看到的情況。
3.複合操作: 幾個必須以原子方式執行的動作的組合,叫做複合操作。如先檢查後執行中,就存在著一組要以原子方式執行的動作。所以先檢查後執行是一個複合操作。如果我們想保證執行緒安全,那麼就要保 證每組複合操作都以原子方式執行。一般來說,我們會通過加鎖來達到這個目的。
1.內建鎖: java提供了一種內建的鎖機制來支援原子性:同步程式碼塊。同步程式碼塊包括兩部分:一個作為鎖的物件參照;一個作為由這個鎖的保護程式碼塊。用關鍵字Synchronized來修飾的方法,是一種包括了整個方法體的同步方式。該程式碼塊的鎖就是呼叫該方法的範例。也就是this。當Synchronized修飾的是靜態方法時,程式碼塊的鎖就是當前類的.CLASS物件。
2.重入: 當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒會被阻塞。然而, 由於內部鎖時可重入的,因此如果某個執行緒試圖獲取一個它已經持有的鎖,那麼這個請求就會成功。重入的一種實現方式就是將鎖與一個計數器還有一個所有者執行緒關聯。當計數器為0時,該鎖沒有被任何執行緒持有,此時任何申請該鎖的執行緒都可以成功,同時JVM將記錄下該鎖的持有者,並將計數器設定為1.當同一執行緒再次獲取這個鎖,計數器將遞增,當執行緒退出同步程式碼塊時,計數器遞減,直到計數器為0,該鎖被釋放。
3.重入的好處:當子類重寫父類別的同步方法時,如下程式碼所示。當執行子類 LoggingWidget的doSomething時,執行緒會獲得鎖。當執行子類的doSomething過程中,又呼叫了父類別的doSomething。若是內建鎖不可重入,則父類別的doSomething會一直等待下去。重入則避免了這種死鎖的發生。
public class Widget{
public synchronized void doSomething(){
...
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomething(){
system.out.println("=================");
super.doSomething();
}
}
變數可以用鎖來保護,這樣可以確保同一時間只有一個執行緒在操作這個變數。只有當該執行緒完成對變數的操作時,其他執行緒才可以對該變數進行操作。
注意:當執行較長時間的計算,或者可能無法快速完成的操作時(網路I/O或者控制檯I/O)一定不要持有鎖