並發(2)線程安全性
一個對象是否需要考慮線程安全,取決於它是否被多個線程訪問。這裏的“訪問”指的是在程序中對象被訪問的方式,而不是對象所實現的功能。要使得對象是線程安全的,需要采用同步機制來協同對象可變狀態的訪問。如果無法實現協同,那麽可能會導致數據破壞以及其它不可預知的結果。
如果沒有合適的同步機制,那麽程序就會出現錯誤,有以下三種方式可以修復這個問題:
1)不在線程之間共享變量
2)將變量改為不可變常量
3)使用同步訪問
一、概念
當多個線程訪問某個類的時候,主動調用的的代碼不需要任何額外的同步或者協同,這個類都能表現出正確的行為,那麽就稱這個類是線程安全的。
二、無狀態
我們看一段代碼
publicclass Test{ public void service(){ int i = 0; System.out.println(i); } }
Test類是無狀態的(沒有任何共享變量),service方法無論怎麽執行都不會出現安全性問題,局部變量i只在當前線程共享,所以無狀態一定是線程安全的。
三、競態條件
在多線程交替執行,執行順序不定的情況下就會發生競態條件,我們先看一段代碼:
public class Test{ private A a = null; public A getA(){ if (a == null){ a = new A(); } return a; } }
這段代碼中雖然判斷了a是否為null,但是在多線程的情況下有可能很多線程都判斷為null,從而導致A被實例化了很多次。這就是典型的競態條件“先檢查後執行”,我們可以預想到存在競態條件的代碼執行結果有時候正確有時候不正確,所以競態條件的執行結果只能看運氣(註意:競態條件與一般語義上的數據競爭不完全一致)。
四、復合操作
我們先來看一個原子操作代碼片段
public class Test { private AtomicLong i = new AtomicLong(0L);public void incrI(){ System.out.println(i.incrementAndGet()); } }
由於AtomicLong是原子對象,其操作都是原子操作,所以以上單個原子操作是不會出現競態條件的,也就是單個原子操作是線程安全的。
下面我們看一個復合操作:
public class Test { private AtomicLong i = new AtomicLong(0L); private AtomicLong count = new AtomicLong(0L); public void incrI(){ if (i.incrementAndGet() >= 1) { count.incrementAndGet(); } } }
代碼中有兩個原子操作組成了復合操作,但是復合操作存在競態條件,即使都是原子操作。我們可以預想到可能出現這樣的情況,例如:
1) 線程1中 i = 1;線程2中 i = 2;
2)線程2中 count + 1 = 1;
1)線程1中 count + 1 = 2;
和我們預想的出現了差別:我們預期結果是i = 1的時候count = 1;i = 2的時候count = 2;也就是當多個原子操作組合復合操作的時候,這個單純的復合操作是沒有原子性的,也就非線程安全。
五、加鎖
為了達到線程安全,我們給上面的復合操作進行加鎖以達到線程安全的目的,例如
public synchronized static void incrI(){ if (i.incrementAndGet() >= 1) { count.incrementAndGet(); } }
同步鎖可以使線程安全,但請註意如果對於需要進行IO等耗時很久的操作的時候,如果采用同步鎖會使性能非常低下。所以在程序中需要考慮線程安全性之外,還需要考慮性能問題而不能盲目為了達到安全性而隨意采用同步鎖等方式。
並發(2)線程安全性