|
設(shè)計(jì)線程安全類,最主要問題是如何把數(shù)據(jù)拆分為多個(gè)獨(dú)立的部分,并為這些部分確定合適的大小。如果每個(gè)部分太小,那么設(shè)計(jì)的類無法做到線程安全。如果每個(gè)部分太大,那么這個(gè)類無法擴(kuò)展。 讓我們通過示例進(jìn)一步說明: 一個(gè)例子 假設(shè)我們要追蹤一個(gè)城市居住了多少人。需要提供兩個(gè)方法,一個(gè)方法獲取當(dāng)前城市的居民人數(shù),另一個(gè)方法把某個(gè)人從一個(gè)城市轉(zhuǎn)移到另一個(gè)城市。接口設(shè)計(jì)如下: public interface CityToCount { static final String[] ALL_CITIES = new String[] { 'Springfield' , 'South Park' }; static final int POPULATION_COUNT = 1000000; void move( String from, String to ); int count(String name);}由于多個(gè)線程需要并行調(diào)用此接口,因此必須思考接口實(shí)現(xiàn)的方案。要么使用 java.util.concurrent.ConcurrentHashMap,要么使用 java.util.HashMap 和單鎖。使用 java.util.concurrent.ConcurrentHashMap 類: move 方法調(diào)用線程安全的 compute 方法減小遷出城市中的居民數(shù)。然后,用 compute 增加遷入城市的居民數(shù)。count 方法中調(diào)用了線程安全的 get 方法。 下面是使用 java.util.HashMap 的實(shí)現(xiàn): public class CityToCountUsingSynchronizedHashMap implements CityToCount { private HashMap<String, Integer> map = new HashMap<String, Integer>(); private Object lock = new Object(); public CityToCountUsingSynchronizedHashMap() { for (String city : ALL_CITIES) { map.put(city, POPULATION_COUNT); } } public void move(String from, String to) { synchronized (lock) { map.compute(from, (key, value) -> { if (value == null) { return POPULATION_COUNT - 1; } return value - 1; }); map.compute(to, (key, value) -> { if (value == null) { return POPULATION_COUNT + 1; } return value + 1; }); } } public int count(String name) { synchronized (lock) { return map.get(name); } }}move 方法同樣使用了 compute 方法增加、減少遷出城市和遷入城市的居民數(shù)。而這一次,由于 compute方法不是線程安全的,因此這兩種方法都被 synchronized 代碼塊包圍。count 方法同樣使用了 get 加 synchronized 代碼塊。 上面兩種解決方案都是線程安全的。 但是,ConcurrentHashMap 方案可以用不同線程并行更新多個(gè)城市。反觀 HashMap 方案,由于 HashMap 代碼完全被鎖包圍,同一時(shí)間只能有一個(gè)線程更新 HashMap。因此,ConcurrentHashMap 方案應(yīng)該擴(kuò)展性更好。讓我們來看看。 太大意味著無法擴(kuò)展 為了比較兩種實(shí)現(xiàn)的可擴(kuò)展性,使用下面的基準(zhǔn)測試: 基準(zhǔn)測試使用 jmh,一種 OpenJDK 微基準(zhǔn)測試框架。在基準(zhǔn)測試中,我把人們從一個(gè)城市遷移到另一個(gè)城市。每個(gè)工作線程都會(huì)更新不同的城市。遷出城市的名稱為線程 ID,遷入城市的名稱為線程 ID 加2。在 Intel i5 4核CPU上運(yùn)行基準(zhǔn)測試,結(jié)果如下: 如我們看到的那樣,使用 ConcurrentHashMap 擴(kuò)展性更好:當(dāng)線程數(shù)大于兩個(gè),該方案性能要比單個(gè)鎖更好。 太小意味著線程不安全 現(xiàn)在,需要增加另一個(gè)方法獲取所有城市的居民總數(shù)。下面用 ConcurrentHashMap 方案實(shí)現(xiàn): public int completeCount() { int completeCount = 0; for (Integer value : map.values()) { completeCount += value; } return completeCount;}要確認(rèn)方案是否線程安全,可以使用以下測試: 需要兩個(gè)線程測試 completeCount 方法是否線程安全。一個(gè)線程把某個(gè)人從 Springfield 移到 South Park。另一個(gè)線程獲取 completeCount并檢查結(jié)果是否符合預(yù)期。 為了測試所有線程交叉的情況,第7行對所有線程使用 while 循環(huán)對 AllInterleavingsvmlens 進(jìn)行測試。執(zhí)行測試可以看到以下錯(cuò)誤: expected:<2000000> but was:<1999999>Vmlens 報(bào)告揭示了問題: 正如看到的那樣,這里的問題在于:人數(shù)統(tǒng)計(jì)已經(jīng)完成,而另一個(gè)線程還在把人從 Springfield 移到 South Park。這時(shí) Springfield 的人數(shù)已經(jīng)減過了,但 South Park 的人數(shù)還沒有增加。 允許并行更新不同城市的人數(shù),在 completeCount 與 move 并行執(zhí)行時(shí),會(huì)導(dǎo)致錯(cuò)誤的結(jié)果。如果提供的方法操作范圍是所有城市,則需要在方法執(zhí)行期間鎖定所有城市。為了支持這樣的方法,需要第二種單鎖解決方案。我們可以實(shí)現(xiàn)一個(gè)線程安全的 countComplete 方法,像下面這樣: 總結(jié) 雖然這個(gè)簡單的例子不能體現(xiàn)數(shù)據(jù)結(jié)構(gòu)的復(fù)雜性,但是示例中體現(xiàn)的思想在現(xiàn)實(shí)世界中也同樣適用。除了在單線程中逐個(gè)字段更新,沒有其它方法可以線程安全地更新多個(gè)關(guān)聯(lián)字段。因此,同時(shí)達(dá)成線程安全與可擴(kuò)展的唯一方法是在數(shù)據(jù)中找到獨(dú)立的部分,然后用多個(gè)線程并行更新。 |
|
|