1、什么是線程安全性
當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些進程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那么就稱這個類是線程安全的。
在線程安全類中封裝了必要的同步機制,因此客戶端無需進一步采取同步錯失。
2、原子性
要編寫線程安全的代碼,其核心在於要對狀態訪問操作進行管理,特別是對共享的和可變的狀態的訪問。當多個線程訪問某個狀態變量,並且其中有一個線程執行寫入操作時,必須采用同步機制來協調這些線程對變量的訪問。無狀態對象一定是線程安全的。
如果我們在無狀態的對象中增加一個狀態時,會出現什么情況呢?假設我們按照以下方式在servlet中增加一個"命中計數器"來管理請求數量:在servlet中增加一個long類型的域,每處理一個請求就在這個值上加1。
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; } }
不幸的是,以上代碼不是線程安全的,因為count++並非是原子操作,實際上,它包含了三個獨立的操作:讀取count的值,將值加1,然后將計算結果寫入count。如果線程A讀到count為10,馬上線程B讀到count也為10,線程A加1寫入后為11,線程B由於已經讀過count值為10,執行加1寫入后依然為11,這樣就丟失了一次計數。
在並發編程中,這種由於不恰當的執行時序而出現不正確的結果是一種非常重要的情況,它有一個正式的名字:競態條件。最常見的競態條件類型就是“先檢查后執行”操作,即通過一個可能失效的觀測結果來決定下一步操作,延遲初始化是競態條件的常見情形:
public class LazyInitRace { private SomeObject instance = null; public SomeObject getInstance() { if(instance == null) instance = new SomeObject(); return instance ; } }
在LazyInitRace中包含競態條件:首先線程A判斷instance為null,然后線程B判斷instance也為null,之后線程A和線程B分別創建對象,這樣對象就進行了兩次初始化,發生錯誤。
要避免靜態條件,就必須在某個線程修改變量時,通過某種方式防止其他線程使用這個變量,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態,而不是在修改狀態的過程中。
在UnsafeCountingFactorizer 例子中,線程不安全的原因是count ++並非原子操作,我們可以使用原子類,確保加操作是原子的,這樣類就是線程安全的了:
public class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count .get() ; } @Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count.incrementAndGet(); } }
AtomicLong是java.util.concurrent.atomic包中的原子變量類,它能夠實現原子的自增操作,這樣就是線程安全的了。
3、加鎖機制
除了使用原子變量的方式外,我們也可以通過加鎖的方式實現線程安全性。還是UnsafeCountingFactorizer,我們只要在它的service方法上增加synchronized關鍵字,那么它就是線程安全的了:
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; } @Override public synchronized void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something count++; } }
在方法上增加synchronized關鍵字后,它能夠保證,同一時間只會有一個線程進入方法體,這樣每個線程就可以全部執行完方法后再退出,方法體內操作就相當於是原子操作了,避免了競態條件錯誤。
以上代碼是線程安全的,但是性能很糟糕,因為我們把整個service都給鎖起來了,同一時刻只能一個線程執行service,並發任務變成了串行任務。其實我們本意只是想把count++變成原子操作,根本就沒必要把整個方法鎖住,只需鎖住count++操作即可:
public class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count ; }
@Override public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException { // do something synchronized(this){ count++; } } }
我們縮小了鎖的范圍,這樣可以更好的增加並發性。
4、可見性
每個線程內部都保有共享變量的副本,當一個線程更新了這個共享變量,另一個線程可能看的到,可能看不到,這就是可見性問題,以下面的代碼為例:
public class NoVisibility { private static boolean ready; private static int number; public static class ReadThread extends Thread { public void run() { while(!ready ) Thread. yield(); System. out.println(number); } } public static void main(String [] args) { new ReadThread().start(); number = 42; ready = true ; } }
以上代碼可能輸出0或者什么也不能輸出。為什么會什么也不能輸出呢?因為我們在主線程中把ready置為true,但是ReadThread中卻不一定能夠讀到我們設置的ready值,所以在ReadThread中Thread.yield()將一直執行下去。為什么可能為0呢?如果ReadThread能夠讀到我們的值,可能先讀到ready值為true,還未讀取更新number值,ReadThread就把保有的number值輸出了,也就是0。
注意,上面的所有內容都是假設,在缺乏同步的情況下,ReadThread和主線程會如何交互,我們是無法預期的,以上兩種情況只是兩種可能性。那么如何避免這種問題呢?很簡單,只要有數據在多個線程之間共享,就使用正確的同步。
4.1、加鎖與可見性
內置鎖可以用於確保某個線程以一種可預測的方式查看另一個線程的執行結果,當線程A進入某同步代碼塊時,線程B隨后進入由同一個鎖保護的同步代碼塊,此時,線程B執行由鎖保護的同步代碼塊時,可以看到線程A之前在同一同步代碼塊中的所有操作結果,如果沒有同步,那么就無法實現上述保證。
加鎖的含義不僅僅局限於互斥行為,還包括內存可見性。為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作的線程都必須在同一個鎖上同步。
4.2、volatile變量
volatile是一種比synchronized關鍵字輕量級的同步機制,volatile關鍵字可以確保變量的更新操作通知到其他線程。
下面是volatile的典型用法:
volatile boolean asleep; ... while(!asleep) doSomeThing();
加鎖機制既可以確保可見性,又可以確保原子性,而volatile變量只能確保可見性。
5、總結
編寫線程安全的代碼,其核心在於要對狀態訪問操作進行管理。編寫線程安全的代碼時,有兩個關注點,一個是原子性問題,一個是可見性問題,要盡量避免競態條件錯誤。