前面德章節(jié)主要介紹java一些底層基礎(chǔ)的并發(fā)實現(xiàn)機制和java的一些并發(fā)基礎(chǔ)知識瀑梗,本章節(jié)主要是用上述的這些知識來構(gòu)建線程安全的類另锋。本章將會把前面介紹的不可變對象(Immutable Object)极祸、對象的線程級限制(Thread Confinement)蛋欣、鎖等技術(shù)綜合運用來構(gòu)建符合各種要求的線程安全的類丐巫。
java監(jiān)視器模式(Java monitor pattern)
下面先看一個典型的java監(jiān)視器模式代碼:
public class PersonList{
private final List<Person> myList = new ArrayList<Person>();
public synchronized void addPerson(Person person){
myList.add(person);
}
public synchronized boolean containsPerson(Person p){
return myList.contains(p);
}
// public synchronized Person get(index i){
// return myList.get(i);
// }
}
代碼中PersonList只有一個成員變量myList锨侯,其被聲明為private final形式的List類型饺谬,通過前面文章 不用鎖,也能實現(xiàn)線程安全的緩存系統(tǒng)的學(xué)習(xí)我們知道final變量的使用可以保證通過PersonList的構(gòu)造方法返回的PersonList類型的實例的成員變量已經(jīng)被初始化完成捂刺。而private的使用保證其成員變量不會被其他線程直接訪問,其addPerson和containsPerson方法都是加鎖的方法,這樣就可以保證同一時間只能有一個線程來操作成員變量myList叠萍。注意到myList是ArrayList類型的變量芝发,其不是線程安全的類,但是通過java的監(jiān)視器模式苛谷,我們實現(xiàn)了PersonList類的線程安全辅鲸。這里我們沒有,也不需要考慮Person類的線程安全性腹殿,因為PersonList類并沒有提供任何方法讓線程獲得myList容器中的Person對象独悴。但是如果PersonList添加有如上被注釋的get方法,那么就需要保證Person類也是線程安全的了锣尉。這就是java監(jiān)視器設(shè)計模式,或許你在平時也用過此模式刻炒,只不過不知道其具體的名字罷了。
從零設(shè)計線程安全的類
為了讓線程安全的類的設(shè)計更有實戰(zhàn)意義自沧,我們從一個真實的開發(fā)需求出發(fā)坟奥,具體需求如下:
某民航公司要求設(shè)計一套客機位置顯示系統(tǒng),要求能夠顯示客機在天空中的經(jīng)度和維度值拇厢;同時每架客機身上都裝有GPS定位系統(tǒng)爱谁,會實時向總控制臺返回自己的位置信息;每架飛機都擁有自己獨一無二的名字孝偎。
從需求上分析我們需要設(shè)計兩種類型的線程:display線程访敌,用來在界面上顯示飛機的位置;update線程衣盾,用來實時更新每架飛機的位置信息這兩個類型的線程由同事A來負(fù)責(zé)寺旺,我們只需要提供相應(yīng)的接口即可。我們將綜合運用前幾章講到的知識和本章的java監(jiān)視器模式來設(shè)計一個符合要求的顯示系統(tǒng)势决。
- java監(jiān)視器模式
飛機在空中的位置可以用經(jīng)度和維度來表示阻塑,其名字可以用一個字符串來表示,于是我們定義好飛機類:
class MutablePlane {
public float x; //維度
public float y; //經(jīng)度
public String name; //飛機名字
public MutablePlane(MutablePlane plane){
this.x = plane.x;
this.y = plane.y;
this.name = plane.name;
}
}
有了飛機的類果复,我們還需要設(shè)計一個類來存儲民航公司的所有飛機的信息的類陈莽,同時此類還要給display線程提供所有飛機信息的接口(方法),此類還要為update類提供更新指定飛機坐標(biāo)位置的接口(方法);當(dāng)然此類可以被display線程,update線程訪問据悔,要保證其線程安全传透,我們首先用java監(jiān)視器模式來設(shè)計此類耘沼,具體類設(shè)計如下:
public class MonitorSystem {
private final Map<String,MutablePlane> planes;
public MonitorSystem(){
//對planes進(jìn)行賦值操作极颓,初始化planes 這里是略寫
planes = new HashMap<String, MutablePlane>();
}
private Map<String,MutablePlane> deepcopy(Map<String,MutablePlane> m){
Map<String,MutablePlane> map = new HashMap<String,MutablePlane>(); //1
for(String name : m.keySet()){ //1
map.put(name,new MutablePlane(m.get(name))); //1
} //1
return Collections.unmodifiableMap(map); //2
}
//update線程使用,用來實時更新指定飛機的位置信息
public synchronized void setLocation(String name, float x, float y) {
MutablePlane mutablePlane = planes.get(name);
if(mutablePlane == null){
throw new RuntimeException("the plane does not exist for name: " + name);
}
mutablePlane.x = x;
mutablePlane.y = y;
}
//display線程使用群嗤,用來在界面上顯示飛機的位置信息
public synchronized Map<String,MutablePlane> getPlanes(){
return deepcopy(planes);
}
}
MonitorSystem類的設(shè)計是一個典型的java監(jiān)視器模式菠隆,我們注意下其getPlanes方法,這個方法調(diào)用了一個deepcopy方法,deepcopy方法先通過//1處的代碼深度復(fù)制planes類骇径,然后通過Collections的unmodifiableMap方法返回一個可讀不可寫的map類躯肌。之所以這么復(fù)雜的設(shè)計getPlanes方法而不是直接返回planes成員變量給display線程是因為planes包含的對象是線程非線程安全的類,我們把planes變量直接暴露給display線程,就相當(dāng)于把線程不安全的MutablePlane對象暴露給別的線程破衔,我們不知道同事A要怎樣設(shè)計他的display線程清女,為了安全起見,我們不能讓其他線程獲得MutablePlane對象的引用晰筛,以防止其對MutablePlane對象做修改嫡丙。這樣實現(xiàn)的getPlanes方法還有一個特點:每次display線程調(diào)用getPlanes方法后得到的飛機位置信息可能已經(jīng)“過時”,在獲得飛機位置信息后读第,update線程可能又對飛機的位置信息做了更新曙博,如果不再次調(diào)用getPlanes方法,是不能獲得新的更新信息的怜瞒。
- 使用代理
觀察MonitorSystem類父泳,因為我們使用的成員變量是非線程安全的HashMap類型,所以我們設(shè)計的getPlanes方和setLocations方法使用了同步吴汪。如果我們把成員變量變成線程安全的:
public class DelegatingSystem {
private final ConcurrentHashMap<String,ImmutablePlane> planes;
private final Map<String,ImmutablePlane> unmodifiableMap;
public DelegatingSystem() {
//對planes進(jìn)行賦值操作惠窄,初始化planes 這里是略寫
this.planes = new ConcurrentHashMap<>();
this.unmodifiableMap = Collections.unmodifiableMap(planes);
}
//update線程使用,用來實時更新指定飛機的位置信息
public void setLocation(String name, float x, float y) {
ImmutablePlane mutablePlane = planes.remove(name);
if(mutablePlane == null){
throw new RuntimeException("the plane does not exist for name: " + name);
}
planes.put(name, new ImmutablePlane(name, x, y));
}
//display線程使用浇坐,用來在界面上顯示飛機的位置信息
public Map<String,ImmutablePlane> getPlanes(){
return unmodifiableMap;
}
}
在DelegatingSystem中我們沒有使用同步(沒有用synchronized關(guān)鍵字聲明方法),因為我們把線程安全代理給了其成員變量planes,planes的類型是ConcurrentHashMap近刘。為了保證DelegatingSystem的線程安全擒贸,我們還要保證其所存儲的飛機信息對象的安全,如果我們繼續(xù)沿用上面的MutablePlane類觉渴,那么display線程可以通過getPlanes拿到MutablePlane對象介劫,而MutablePlane對象是非線程安全的。為此我們定義了新的不可變對象ImmutablePlane如下:
public class ImmutablePlane {
public final float x;
public final float y;
public final String name;
public ImmutablePlane(String name, float x, float y) {
this.x = x;
this.y = y;
this.name = name;
}
}
這樣就可以保證DelegatingSystem的線程安全案淋,當(dāng)然我們?nèi)匀豢梢韵隡onitorSystem那么使用MutablePlane類座韵,但其getPlanes方法就需要返回的是對planes成員變量的深度拷貝,就像MonitorSystem的getPlanes方法一樣踢京。我們這里用了線程安全的ImmutablePlane類誉碴,來避免深度拷貝,這樣在飛機特別多的情況下瓣距,可以節(jié)省深度拷貝方法的調(diào)用時間黔帕,從而提高響應(yīng)效率,當(dāng)然DelegatingSystem還有另外一個特價蹈丸,就是display線程能實時獲得update線程更新飛機后的最新位置信息成黄,而不需要重新調(diào)用getPlanes方法呐芥。兩種方式各有利弊,需要具體情況下具體選擇奋岁。
利用現(xiàn)有線程安全的類
我們從DelegatingSystem類的設(shè)計中可以看到其使用了java基礎(chǔ)類庫中的ConcurrentHashMap類來保證其線程安全思瘟,這往往是最高效最簡單也是最安全的方式。
- 簡單:它避免也我們像MonitorSystem類那樣每個方法都要自己設(shè)計同步邏輯闻伶,像setLocation和getPlanes方法用到了鎖滨攻,但deepCopy就可以不用鎖,這對開發(fā)者的并發(fā)知識要求很高蓝翰,設(shè)計起來不是很簡單铡买。
- 高效:java的concurrent包中的各種類設(shè)計的非常精巧,在保證線程安全的同時有可以有很高的并發(fā)率霎箍,我們很難也沒有比較設(shè)計出比先有并發(fā)類更高效的線程安全的類奇钞。
- 安全:java基礎(chǔ)庫里面的類,都是經(jīng)過千錘百煉的漂坏。我們自己設(shè)計的類往往因為測試不夠或者設(shè)計不夠縝密景埃,而導(dǎo)致意想不到的問題,而在多線程環(huán)境下線程安全的類更是難上加難顶别,需要開發(fā)者能深入理解java的并發(fā)機制谷徙,同時準(zhǔn)確把握設(shè)計需求,稍有不慎就可能留下bug驯绎,而在 《java并發(fā)編程實戰(zhàn)》第二章:線程安全 開頭我們就介紹了多線程bug的嚴(yán)重危害完慧,這里不再贅述。
當(dāng)然利用現(xiàn)有線程安全的類構(gòu)建新的線程安全的類剩失,也并不是沒有缺點和需要注意的事項屈尼,下面通過幾種常見的構(gòu)建方式也說說其缺點和需要注意的事項。假設(shè)我們有如下需求:設(shè)計一個線程安全的類拴孤,能夠?qū)崿F(xiàn)線程安全的讀寫同時還提供額外的方法put-if-absent脾歧,具體語意為:向其中添加元素如果此類中不存在此元素就添加,如果已經(jīng)存在演熟,就不添加此元素鞭执。我們用不同的方法來實現(xiàn)這個功能。
- 改造現(xiàn)有類
我們發(fā)現(xiàn)有很多現(xiàn)有的線程安全的類可以滿足需求的讀寫方法的要求芒粹,我們需要做的是設(shè)計一個線程安全的put-if-absent方法兄纺,我們可以直接選擇修改一個線程安全的類,比如Vector,在其中添加一個線程安全的方法來實現(xiàn)put-if-absent的邏輯化漆。
這種方式的優(yōu)點是可以最大限度的保持代碼的健壯性估脆,前提是你能獲得CopyOnWriteArrayList的修改權(quán)限,而且對其線程安全策略十分了解获三。 - 繼承現(xiàn)有類
我們可以構(gòu)造如下的類:
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if(absent){
add(x);
}
return absent;
}
}
這樣設(shè)計的BetterVector類可以實現(xiàn)上述的要求旁蔼,但是它健壯性就不如前面德直接改造現(xiàn)有類。因為我們putIfAbsent方法用的對象的內(nèi)置鎖,而且Vector類的確也是用的內(nèi)置鎖來實現(xiàn)的線程安全疙教。但是如果我們繼承的不是java基礎(chǔ)的線程安全類棺聊,我們繼承了別的線程安全的類A,使用了其線程安全策略贞谓。如果之后類A更換線程安全策略限佩,比如從使用內(nèi)置鎖變?yōu)槭褂肦eentantLock來實現(xiàn)其線程安全策略,那么我們的代碼的線程安全性就會被無聲無息的破壞裸弦。這也是繼承現(xiàn)有類來實現(xiàn)線程安全的缺點之一祟同。
最后的最后,需要強調(diào)的是不管你是從0開始設(shè)計了一個線程安全的類理疙,還是用java現(xiàn)有的線程安全的類設(shè)計出來一個類晕城,我們都要需要為我們設(shè)計的類寫好說明文檔,這樣不僅僅是利于后面的維護(hù)人員日常維護(hù)窖贤,也對我們后續(xù)開發(fā)和查閱代碼也相當(dāng)重要砖顷。