4.1 設(shè)計(jì)線程安全的類
在設(shè)計(jì)線程安全的類的過程中,需要包含以下的三個(gè)基本的要素:
- 找出構(gòu)成對象狀態(tài)的所有變量
- 找出約束變量的不可變性
- 建立對象狀態(tài)的并發(fā)訪問管理
public final class Counter{
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long setValue(){
if (value == long.MAX_VALUE)
throw new IllegalStateException("Counter overflow");
return ++value;
}
}
如上,我們構(gòu)造了一個(gè)線程安全的類,我們來找出它所滿足的三個(gè)條件:
- 變量:value變量
- 約束:改變value前,判斷value是否達(dá)到最大值```if (value == long.MAX_VALUE)
- 并發(fā)管理:通過synchronized關(guān)鍵字
##### 4.1.1 收集同步需求
> 如果不了解對象的不變性條件與后驗(yàn)條件,那么就不能保證線程的安全性。要滿足在狀態(tài)變量的有效值或狀態(tài)轉(zhuǎn)換上的各種約束條件,就需要借助于原子性和封裝性。
我們先來理解這兩個(gè)名詞:
- 不可性條件:在許多類中都定義了一些不可變條件,用于判斷狀態(tài)是否有效。比如,在上面的例子中,value的值必須滿足在```Long.MIN_VALUE```和```Long.MAX_VALUE```之間。
- 后驗(yàn)條件:比如上面counter中當(dāng)前狀態(tài)為17,那么下一個(gè)有序狀態(tài)只能為18。當(dāng)下一個(gè)狀態(tài)需要依賴上一個(gè)狀態(tài)時(shí),這個(gè)操作就必須是一個(gè)復(fù)合操作。
##### 4.1.2 依賴狀態(tài)的操作
> 如果在某個(gè)操作中包含有基于狀態(tài)的先驗(yàn)條件,那么這個(gè)操作就稱為“依賴狀態(tài)的操作”。比如,在不能從空隊(duì)列中刪除元素,因此,在刪除元素之前需要檢查隊(duì)列是否為空。
##### 4.1.3 狀態(tài)的所有權(quán)
>狀態(tài)變量的所有者將決定采用何種加鎖協(xié)議來維持狀態(tài)的完整性。所有權(quán)意味著控制權(quán)。但,如果發(fā)布了某個(gè)可變對象的引用,那么就不再擁有獨(dú)立的控制權(quán),最多是“共享控制權(quán)”。
#### 4.2 實(shí)例封閉
> 將數(shù)據(jù)封裝在對象內(nèi)部,可以將數(shù)據(jù)的訪問限制在對象的方法上,從而更容易確保在訪問數(shù)據(jù)時(shí)總能持有正確的鎖。
注意:被封閉的對象一定不能超過它們既定的作用域。對象可以封閉在類的一個(gè)實(shí)例(比如私有成員)中,或者封閉在某個(gè)作用域內(nèi)(比如一個(gè)局部變量),再或者封閉在線程內(nèi)。
public class PersonSet{
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
public synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}
如上,我們將PersonSet進(jìn)行了實(shí)例封閉,將數(shù)據(jù)mySet設(shè)置為私有成員,并通過方法來對數(shù)據(jù)進(jìn)行訪問。
##### 4.2.1 Java監(jiān)視器模式
> 對于任何一種鎖對象,只要自始自終都使用該鎖對象,都可以用來保護(hù)對象的狀態(tài)。
public class privateLock{
private final Object myLock = new Object();
Widget widget;
void someMethod(){
synchronized(myLock){
//訪問或修改Widget的狀態(tài)
}
}
}
如上為通過一個(gè)私有鎖來保護(hù)狀態(tài)
###### 示例:基于監(jiān)視器模式的車輛追蹤
我們要求視圖線程和更新線程并發(fā)地訪問數(shù)據(jù)模型,因此該模型必須是線程安全的。
- 記錄軌跡的點(diǎn):
public class MutablePoint {
public int x,y;
public MutablePoint(){ x = 0; y = 0;}
public MutablePoint(MutablePoint p){
this.x = p.x;
this.y = p.y;
}
}
- 實(shí)現(xiàn)車輛追蹤:
public class MonitorVehicleTracker {
private final Map<String, MutablePoint> locations;
//構(gòu)造函數(shù)
public MonitorVehicleTracker(Map<String,MutablePoint> locations){
this.locations = locations;
}
//返回?cái)?shù)據(jù)的拷貝對象
public synchronized Map<String,MutablePoint> getLocations(){
return deepCopy(locations);
}
//對數(shù)據(jù)進(jìn)行更新
public synchronized void setLocations(String id, int x, int y){
MutablePoint loc = locations.get(id);
if (loc == null)
throw new IllegalArgumentException("No such ID: " + id);
loc.x = x;
loc.y = y;
}
//實(shí)現(xiàn)對象數(shù)據(jù)的拷貝
private Map<String,MutablePoint> deepCopy(Map<String, MutablePoint> map){
Map<String,MutablePoint> result = new HashMap<>();
for (String id : map.keySet())
result.put(id,new MutablePoint(map.get(id)));
return Collections.unmodifiableMap(result);
}
}
- 模擬更新和訪問數(shù)據(jù):
public static void main(String[] args){
//初始化數(shù)據(jù)
Map<String,MutablePoint> map = new HashMap<>();
map.put("test1",new MutablePoint());
map.put("test2",new MutablePoint());
map.put("test3",new MutablePoint());
MonitorVehicleTracker tracker = new MonitorVehicleTracker(map);
//設(shè)置更新線程,每隔10s更新一次數(shù)據(jù)
Thread set_thread = new Thread(){
@Override
public void run() {
while (true){
Map<String,MutablePoint> locations = tracker.getLocations();
for (String key : locations.keySet()){
MutablePoint p = locations.get(key);
int dx = (int) (Math.random() * 10);
int dy = (int) (Math.random() * 10);
tracker.setLocations(key, p.x+dx, p.y+dy);
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//設(shè)置視圖線程,每隔10s獲取一次數(shù)據(jù),并顯示
Thread get_thread = new Thread(){
@Override
public void run() {
while(true){
Map<String,MutablePoint> locations = tracker.getLocations();
for (String key : locations.keySet()){
MutablePoint p = locations.get(key);
System.out.println("key: " + key + " value: (" + p.x
+ ","+p.y + ")");
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
set_thread.start();
get_thread.start();
}
- 總結(jié):對于上面的模式,類MutablePoint不是線程安全的,但追蹤器類是線程安全的。因?yàn)樗膍ap對象和可變的Point對象都未曾發(fā)布過。當(dāng)需要返回車輛的位置時(shí),通過MutablePoint拷貝構(gòu)造函數(shù)來復(fù)制正確的值,從而生成一個(gè)新的對象。
- 評價(jià):優(yōu)點(diǎn)是location集合上內(nèi)部的數(shù)據(jù)是一致的,缺點(diǎn)是每次調(diào)用getLocation都需要復(fù)制數(shù)據(jù),這將影響到性能。
#### 4.3 線程安全性的委托
public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);
public long getCount() {return count.get();}
public void service(ServletRequest req, ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factors(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
> 如上,我們將CountingFactorizer類的線程安全性委托給AtomicLong來保證:之所以CountingFactorizer是安全的,是因?yàn)锳tomicLong是安全的。
###### 4.3.1 示例:基于委托的車輛追蹤器
- 我們用不可變的Point類代替MutablePoint類:
public class Point{
public final int x,y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
}
這樣能保證在返回location時(shí),不需要復(fù)制。因?yàn)椴豢勺兊闹凳强梢员蛔杂傻毓蚕怼?
- 將線程安全委托給ConcurrentHashMap
public class DelegatingVehicleTracker{
private final ConcurrentHashMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String,Point> map){
locations = new ConcurrentHashMap<String,Point>(map);
unmodifiableMap = Collections.unmodifiableMap(map);
}
public Map<String,Point> getLocations(){
return unmodifiableMap;
}
public Point getLocation(String id){
return locations.get(id);
}
public void setLocation(String id, int x, int y){
if (locations.replace(id,new Point(x,y)) == null)
throw new IllegalArgumentException("Invalid vehicle name: " + id);
}
}
我們可以看到將線程安全性交給了安全容器ConcurrentHashMap,從而免去了對方法的加鎖。
##### 4.3.2 獨(dú)立的狀態(tài)變量
> 我們可以將線程安全性委托給多個(gè)狀態(tài)變量,只要這些變量是彼此獨(dú)立的,即組合而成的類并不會在其包含的多個(gè)狀態(tài)變量上增加任何不變性條件。
public class VisualComponent{
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener){
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener){
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener){
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener){
mouseListeners.remove(listener);
}
}
如上,我們將鍵盤和鼠標(biāo)監(jiān)聽器列表都委托給CopyOnWriteArrayList,因?yàn)閮烧咧g是相互獨(dú)立的,因此不會增加不變性條件。
##### 4.3.3 當(dāng)委托失效時(shí)
> 如果一個(gè)類是由多個(gè)獨(dú)立且線程安全的狀態(tài)變量組成,并且在所有的操作中都不包含無效狀態(tài),那么可以將線程安全性委托給底層的狀態(tài)變量。
public class NumberRange{
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i){
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i){
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
lower.set(i);
}
public boolean isInRange(int i){
return (i >= lower.get() && i <= upper.get());
}
}
如上,雖然AtomicInteger是線程安全的,但經(jīng)過組合得到的類卻不安全。由于狀態(tài)變量lower和upper并不是彼此獨(dú)立的,因此NumberRange不能將線程安全性委托給它的安全狀態(tài)變量。
如果某個(gè)類含有復(fù)合操作,僅靠委托并不足以實(shí)現(xiàn)線程安全性時(shí),需要提供自己的加鎖機(jī)制以保證這些復(fù)合操作都是原子性。
##### 4.3.4 發(fā)布底部的狀態(tài)變量
> 如果一個(gè)這個(gè)變量是線程安全的,并且沒有任何不變性條件來約束它的值,在變量的操作上也不存在任何不允許的狀態(tài)轉(zhuǎn)換,那么就可以安全地發(fā)布這個(gè)變量。
##### 4.3.5 發(fā)布狀態(tài)的車輛追蹤器
- 定義一個(gè)安全且可變的Point類:
public class SafePoint{
private int x,y;
private SafePoint(int[] a) {this(a[0],a[1]);}
public SafePoint(int x,int y){
this.x = x;
this.y = y;
}
public synchronized int[] get(){
return new int[] {x,y};
}
public synchronized void set(int x,int y){
this.x = x;
this.y = y;
}
}
注意:我們這里將x和y綁定在一起,因?yàn)槿绻謩e為x和y提供get方法,那么在獲得這兩個(gè)不同坐標(biāo)的操作之間,x和y的值發(fā)生變化,從而導(dǎo)致調(diào)用者看到不一致的值。
- 安全發(fā)布底層狀態(tài)的車輛追蹤器:
public class PublishingVehicleTracker{
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String,SafePoint> map){
locations = new ConcurrentHashMap<String,SafePoint>(map);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String,SafePoint> getLocations(){
return unmodifiableMap;
}
public SafePoint getLocation(String id){
return locations.get(id);
}
public void setLocation(String id, int x, int y){
if (!locations.containsKey(id))
throw new IllegalArgumentException(
"Invalid vehicle name: " + id);
locations.get(id).set(x,y);
}
}
注意:PublishingVehicleTracker將其線程安全性委托給底層的ConcurrentHashMap,只是Map的元素是線程安全且可變的Point。getLocation方法返回底層Map對象的一個(gè)不可變副本。調(diào)用者不能增加或刪除車輛,但卻可以通過修改返回Map中的SafePoint值來改變車輛的位置。
#### 4.4 在現(xiàn)有的線程安全類中添加功能
> 假設(shè)需要一個(gè)線程安全的鏈表,它需要提供一個(gè)原子的“若沒有則添加”的操作。
- 對原有的類進(jìn)行擴(kuò)展:
public class BetterVector<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
評價(jià):如果底層的類改變了同步策略并選擇了不同的鎖來保護(hù)它的狀態(tài)量,那么子類會被破壞。因?yàn)樵谕讲呗愿淖兒笏鼰o法再使用正確的鎖來控制對基類狀態(tài)的并發(fā)訪問。
- 擴(kuò)展類的功能:
public class ListHelper<E>{
public List<E> list =
Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
評價(jià):該中方式并不能保證線程的安全性,因?yàn)長ist所用的鎖和ListHelper所用的鎖不是同一個(gè)鎖。這意味著putIfAbsent相對于list的其他操作來說不是原子的。
- 客戶端加鎖:對于使用某個(gè)對象X的客戶端代碼,使用X本身用于保護(hù)其狀態(tài)的鎖來保護(hù)這段客戶代碼。
public class ListHelper<E>{
public List<E> list =
Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x){
synchronized(list){
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
如此,就能保證ListHelper所用的鎖和list所用的鎖是一致的。
##### 4.4.2 組合:實(shí)現(xiàn)接口的方式
public class ImprovedList<T> implements List<T>{
private final List<T> list;
public ImprovedList(List<T> list){
this.list = list;
}
public synchronized boolean putIfAbsent(T x){
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
ImprovedList通過自身的內(nèi)置鎖增加了一層額外的加鎖來實(shí)現(xiàn)線程的安全性。