《JAVA併發程式設計實戰》物件的組合
文章目錄
設計執行緒安全的類
在設計執行緒安全類的過程中,需要包含以下三個基本要素:
- 找出構成物件狀態的所有變數
- 找出約束狀態變數的不變性條件
- 建立物件狀態的併發訪問管理策略
找出構成物件狀態的所有變數
分析物件的狀態,首先從物件的域開始:
- 如果物件所有域都是基本型別的變數,那麼這些域構成物件的全部狀態。對於含有n個基本型別域的物件,其狀態就是這些域構成的n元組。
- 如果在物件的域中引用了其他物件,那麼該物件的狀態將包含被引用物件的域。
示例
public class Person{
private int id;
}
這個物件的域為id,這個域就是Person物件的全部狀態。
public class Coordinate{
private int x;
private int y;
}
這個物件有兩個域x,y;Coordinate物件的狀態為二元組(x,y);
public class Diary{
private Person person;
}
這個物件有兩個域person,這個域都是引用型別。所以狀態包含person物件裡的域id.
找出約束狀態變數的不變性條件
利用不變性條件判斷狀態是否有效,例如整型int型別,狀態空間為Integer.MIN_VALUE~Integer.MAX_VALUE
後驗條件判斷遷移是否有效。如果在某個操作中存在無效的狀態轉換,那麼該操作必須是原子的。
例項封閉
如果某物件不是執行緒安全的,一般可以通過兩種技術使其在多執行緒程式中安全的使用:
- 確保該物件只能由單個執行緒訪問(執行緒封閉)
- 通過一個鎖來保護該物件的所有訪問。
public class PersonSet{
private final Set<Person> mySet = new HashSet<>();
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p) {
return mySet.contains(p);
}
}
這個類是執行緒安全的,雖然HashSet是非執行緒安全的,但是mySet是私有的,且不會被get,因此HashSet是被封閉在PersonSet中的。而且能訪問mySet的addPerson和containsPerson在執行時需要訪問PersonSet的內建鎖,因此PersonSet是個執行緒安全的類。需要注意的是這裡的Person類如果是可變的,那麼在訪問從PersonSet中獲得的Person物件時,還需要額外的同步。
java監視器模式
遵循java監視器模式的物件會把所有可變狀態都封裝起來,並由物件的內建鎖保護。
私有的鎖物件:
//執行緒安全
public class PrivateLock{
private Object myLock = new Object();
Widget widget;
void someMethod() {
synchronized(myLock) {
// 訪問或修改Widget的狀態
}
}
}
使用私有的鎖物件而不是物件的內建鎖,有以下優點:
- 私有的鎖物件可以將鎖封裝起來,使客戶程式碼無法獲得鎖,但客戶程式碼可以通過公有方法來訪問鎖,以便參與到它的同步策略中。
示例:車輛追蹤
public class MonitorVehicleTracker {
private final Map<String,MutablePoint> locations;
public MonitorVehicleTracker(Map<String,MutablePoint> locations) {
this.locations = locations;
}
public synchronized Map<String,MutablePoint> getLocations() {
return deepCopy((locations);
}
public synchronized MutablePoint getLocation(String id) {
MutablePoint loc = locations.get(id);
return loc == null ? null : new MutablePoint(loc);
}
public synchronized void setLocation(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;
}
private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m) {
Map<String,MutablePoint> res = new HashMap<String,MutablePoint>();
for(String id : m.keySet()) {
res.put(id,new MutablePoint(m.get(id)));
}
return Collections.unmodifiableMap(res);
}
// 這個類非執行緒安全
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;
}
}
}
執行緒安全性的委託
public class CountingFactorizer{
public AtomicLong value;
}
對於這樣一個類,由於AtomicLong是執行緒安全的,而且CountingFactorizer只包含value一個狀態,所以CountingFactorizer是執行緒安全的。我們將這個過程稱為執行緒安全性的委託,CountingFactorizer將它的執行緒安全性委託給AtomicLong保證。
示例:基於委託的車輛追蹤器
public class DelegatingVehicleTracker {
private final ConcurrentMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String,Point> points) {
locations = new ConcurrentHashMap<String,Point>(points);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
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);
}
}
}
class Point {
public final int x,y;
public Point(int x,int y) {
this.x = x;
this.y = y;
}
}
如果使用最初的MutablePoint而不是Point類,就會破壞封裝性,因為getLocations會釋出一個指向可變狀態的引用,而這個引用不是執行緒安全的。
委託給多個獨立的狀態變數
前面的委託都是針對單個狀態變數,我們還可以將執行緒安全性委託給多個狀態變數。
public class VisualComponent {
private final List<KeyBoardListener> keyBoardListener = new CopyOnWriteArrayList<KeyBoardListener>();
private final List<MouseListener> keyBoardListener = new CopyOnWriteArrayList<MouseListener>();
public void addKeyBoardListener(KeyBoardListener listener) {
keyBoardListener.add(listener);
}
public void addMouseListener(MouseListener listener) {
MouseListener.add(listener);
}
public void removeKeyBoardListener(KeyBoardListener listener) {
keyBoardListener.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
MouseListener.remove(listener);
}
}
委託失效
當有約束條件時,委託容易失效
public class NumberRange{
// 不變性條件:lower <= upper
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 low 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");
}
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
NumberRange不是執行緒安全的,沒有維持對下界和上界進行約束的不變性條件。
setLower 和 setUpper都嘗試維持不變性,但是都沒有做到,因為他們是採用“先檢查後執行”的操作,但沒有使用足夠的加鎖機制保證這些操作的原子性。如果一個執行緒呼叫setLower(5),另一個執行緒呼叫setUpper(4),如果執行時序錯誤,那麼兩個方法同時通過檢查進行設定就會導致upper<lower。不符合約束。
如果一個類是由多個獨立且執行緒安全的狀態變數組成,並且在所有的操作中都不包含無效狀態轉換,那麼可以將執行緒安全性委託給底層的狀態變數。
釋出底層的狀態變數
根據類對底層狀態變數施加的不變性條件,我們才可以釋出這些變數從而使其他類能修改他們。
如果一個狀態變數是執行緒安全的,並且沒有任何不變性條件約束它的值,在變數的操作上也不存在任何不允許的狀態轉換,那麼就可以安全的釋出這個變數。
示例:釋出狀態的車輛追蹤器
public class PublishingVehicleTracker {
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String,SafePoint> locations) {
this.locations = new ConcurrentHashMap<String,SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.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);
}
}
class SafePoint {
private int x,y;
private SafePoint(int[] a){
this(a[0],a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
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;
}
}
SafePoint(SafePoint p)獲取p的拷貝,方法體內不直接呼叫SafePoint(int x,int y)的原因是因為避免產生競態條件,私有建構函式可以避免這種競態條件。(私有建構函式捕獲模式)
PublishingVehicleTracker將其執行緒安全性委託給底層的ConcurrentHashMap,不同的是Map中的元素是執行緒安全且可變的Safeoint,而並非不可變的。getLocation返回底層Map物件的一個不可變副本。呼叫者可以修改Map中的SafePoint值改變車輛的位置。
在現有的執行緒安全類中新增功能
假設需要一個執行緒安全的連結串列,它需要提供一個原子的“若沒有則新增”操作。
因為需要的是一個執行緒安全的類,那麼這種“先查詢後執行”的操作就要是原子的.
最安全的方式是修改原始的類,但這通常無法做到。另一個方法就是擴充這個類,但是並非所有的類都像Vector這樣將狀態像子類公開。
public class BetterVector<E> extends Vector<E> {
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if(absent) {
add(x);
}
return absent;
}
}
客戶端加鎖機制
對於由Collections.synchronizedList封裝的ArrayList,在原始類中新增一個方法或者對類進行擴充套件都不行,第三種策略是擴充套件類的功能,但不是擴充套件類本身,而是將擴充套件程式碼放入一個“輔助類”中。
一個錯誤的做法:
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;
}
}
錯誤的原因是synchronized沒有鎖住list,在執行putIfAbsent時可能list會被另外一個執行緒修改。
如果想要正確執行該方法,必須使List在實現客戶端加鎖或外部加鎖時使用同一個鎖。
客戶端加鎖是值,對使用某個物件X的客戶端程式碼,使用X本身用於保護其狀態的鎖來保護這段客戶程式碼。在這裡X指的是list。
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;
}
}
}
通過新增一個原子操作擴充套件類是脆弱的,因為它將類的加鎖程式碼分佈到多個類中。客戶端加鎖卻更加脆弱因為它將類X的加鎖程式碼放到和X完全無關的其他類中。
組合
組合是一種更好的方法。
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 contains = list.contains(x);
if(contains) {
list.add(x);
}
return !contains;
}
public synchronized void clear() {
list.clear();
}
}
通過自身的內建鎖增加了一層額外的加鎖。我們使用了java監視器模式來封裝現有List,並且只要在類中擁有指向底部List的唯一外部引用,就能確保執行緒安全性。
將同步策略文件化
在文件中說明客戶程式碼需要了解的執行緒安全性保證,以及程式碼維護人員需要了解的同步策略。
在設計同步策略時需要考慮多個方面:例如,將哪些變數宣告為volatile型別,哪些變數用鎖來保護,哪些鎖保護哪些變數,哪些變數必須是不可變的或者被封閉線上程中的,哪些操作必須是原子操作等。
相關文章
- Java併發程式設計實戰--事實不可變物件Java程式設計物件
- Java併發程式設計實戰--顯式的Condition物件Java程式設計物件
- Java併發程式設計實戰Java程式設計
- Java併發程式設計實戰--FutureTaskJava程式設計
- java併發程式設計實戰-第三章-物件的共享Java程式設計物件
- Java併發程式設計實戰--Amdahl定律Java程式設計
- Java併發系列—併發程式設計挑戰Java程式設計
- Java併發程式設計實戰總結 (一)Java程式設計
- 《JAVA併發程式設計實戰》顯式鎖Java程式設計
- Java併發程式設計實戰(4)- 死鎖Java程式設計
- Java併發程式設計實戰——讀後感Java程式設計
- Java併發程式設計實戰--筆記三Java程式設計筆記
- Java併發程式設計實戰--筆記四Java程式設計筆記
- Java併發程式設計實戰--筆記二Java程式設計筆記
- Java併發程式設計實戰--this引用逸出Java程式設計
- Java併發程式設計實戰--筆記一Java程式設計筆記
- Java併發程式設計實戰--閉鎖 CountDownLatchJava程式設計CountDownLatch
- 【面試實戰】# 併發程式設計面試程式設計
- 《JAVA併發程式設計實戰》任務執行Java程式設計
- 《JAVA併發程式設計實戰》取消和關閉Java程式設計
- 實戰Java高併發程式設計模式視訊Java程式設計設計模式
- Java併發程式設計實戰--協作物件間的死鎖與開放呼叫Java程式設計物件
- Java併發程式設計實戰--計數訊號量(Semaphore)Java程式設計
- Java併發程式設計 - 第十一章 Java併發程式設計實踐Java程式設計
- 《JAVA併發程式設計實戰》基礎構建模組Java程式設計
- 併發程式設計實戰——鎖分段程式設計
- Java併發程式設計實踐Java程式設計
- Java併發程式設計實戰--讀書筆記(目錄)Java程式設計筆記
- java 併發程式設計Java程式設計
- Java併發程式設計Java程式設計
- Java併發程式設計實戰 04死鎖了怎麼辦?Java程式設計
- Java併發程式設計實戰-王寶令-極客時間Java程式設計
- Java併發程式設計實戰(5)- 執行緒生命週期Java程式設計執行緒
- Java併發程式設計---java規範與模式下的併發程式設計1.1Java程式設計模式
- Java併發程式設計實戰系列16之Java記憶體模型(JMM)Java程式設計記憶體模型
- JAVA實現網路程式設計之併發程式設計Java程式設計
- 【Java併發程式設計】併發程式設計大合集-值得收藏Java程式設計
- Java併發程式設計實踐-this溢位Java程式設計