[Java 併發編程實戰] 設計線程安全的類的三個方式(含代碼)

来源:https://www.cnblogs.com/seaicelin/archive/2018/06/07/9148746.html
-Advertisement-
Play Games

發奮忘食,樂以忘優,不知老之將至。———《論語》 前面幾篇已經介紹了關於線程安全和同步的相關知識,那麼有了這些概念,我們就可以開始著手設計線程安全的類。本文將介紹構建線程安全類的幾個方法,並說明他的區別。 我要講的這幾個構建線程安全類的方式是: 另外,在設計線程安全類的過程中,我們需要考慮下麵三個基 ...


發奮忘食,樂以忘優,不知老之將至。———《論語》

前面幾篇已經介紹了關於線程安全和同步的相關知識,那麼有了這些概念,我們就可以開始著手設計線程安全的類。本文將介紹構建線程安全類的幾個方法,並說明他的區別。

我要講的這幾個構建線程安全類的方式是:

  1. 實例封閉。
  2. 線程安全性的委托。
  3. 現有的線程安全類添加功能。

另外,在設計線程安全類的過程中,我們需要考慮下麵三個基本要素,遵循這三個步驟:

  • 找出構成對象狀態的所有變數。
  • 找出約束狀態變數的不變性條件。
  • 建立對象狀態的併發訪問策略。

以上,就是這篇文章主要講解的內容,下麵章節分三個構建方法逐步展開說明,逐個分析,並附上自己測試過的實例代碼,確保這篇文章分享的內容是經過驗證的。

實例封閉

意思是將數據封裝在對象內部,它將數據的訪問限制在對象的方法上,從而更容易確保線程在訪問數據時總能持有正確的鎖。當一個非線程安全對象被封裝到另一對象中時,能夠訪問被封裝對象的所有代碼路徑都是已知的。這和在整個程式中直接訪問非線程對象相比,更易於對代碼進行分析。下麵代碼清單就是一個實例封閉的例子:

 1import java.util.ArrayList;
2
3//ThreadSafe
4public class PointList{
5
6 //非線程安全對象 myList
7 private final ArrayList<SafePoint> myList = new ArrayList<SafePoint>();
8
9 //所有訪問 myList 的方法都是用同步鎖,確保線程安全
10 public synchronized void addPoint(SafePoint p) {
11 myList.add(p);
12 }
13 //所有訪問 myList 的方法都是用同步鎖,確保線程安全
14 public synchronized boolean containsPoint(SafePoint p) {
15 return myList.contains(p);
16 }
17 //所有訪問 myList 的方法都是用同步鎖,確保線程安全
18 //發佈SafePoint
19 public synchronized SafePoint getPoint(int i) {
20 return myList.get(i);
21 }
22
23 //ThreadSafe(可發佈的可變線程安全對象)
24 class SafePoint{
25 private int x;
26 private int y;
27
28 private SafePoint(int[] a) {this(a[0], a[1]);}
29
30 public SafePoint(SafePoint p) {this(p.get());}
31
32 public SafePoint(int x, int y) {
33 this.x = x;
34 this.y = y;
35 }
36 //使用同步鎖,確保線程安全
37 public synchronized int[] get() {
38 return new int[] {x, y};
39 }
40 //使用同步鎖,確保線程安全
41 public synchronized void set(int x, int y) {
42 this.x = x;
43 this.y = y;
44 }
45 }
46}

PointList 的狀態由 ArrayList 來管理,但是 ArrayList 並非線程安全的。由於 ArrayList 私有並且不會逸出,因此 ArrayList 被封閉在 PointList 中。唯一能夠訪問 ArrayList 的路徑都上同步鎖了,也就是說 ArrayList 的狀態完全有 PointList 內置鎖保護,因而 PointList 是一個線程安全的類。Point 類的安全性放到後面討論。

從這裡例子可以看出,實例封閉可以非常簡單的構建出線程安全的類。封閉機制更易於構造線程安全的類,因為當封閉類的狀態時,在分析類的線程安全性時就無需檢查整個程式。當然,如果將一個本該封閉的對象發佈出去,那麼也會破壞封閉性。

線程安全性的委托

如果類中的各個狀態已經是線程安全的,那麼是否需要再增加一個線程安全層的封裝呢?
具體問題具體分析,這種需要視情況而定。

1) 如果各個狀態變數是相互獨立的並且互不依賴,並且沒有複合操作,那麼可以將線程安全性委托給底層的狀態變數。如將安全性委托給 value:

1import java.util.concurrent.atomic.AtomicInteger;
2
3public class SafeSequene{
4 private value = new AtomicInteger(0);
5 //返回一個唯一的數值
6 public synchronized int getNext(){
7 return value.incrementAndGet();
8 }
9}

2) 如果各個狀態變數之間存在依賴關係,並且存在複合操作,那麼是非線程安全的。來看下麵一個例子,NumberRange 這個類的各個狀態組成部分都是線程安全的,但是存在狀態之間的依賴關係,並非互相獨立,所以也是非線程安全的。

 1import java.util.concurrent.atomic.AtomicInteger;
2
3public class NumberRange{
4
5 //不變性條件:lower <= upper
6 private final AtomicInteger lower = new AtomicInteger(0);//線程安全類
7 private final AtomicInteger upper = new AtomicInteger(0);//線程安全類
8
9 private static boolean flag = true;
10
11 private static volatile boolean stopAllThread = false; //檢測到無效狀態,停止所有線程並輸出,此時lower > upper
12
13 private static int count = 3; //非線程安全,但是不必理會,不影響我們測試
14
15 //檢查然後更新
16 public void setLower(int i) {
17 if(i <= upper.get()) { //lower依賴upper的值,有可能upper的值已經失效
18 lower.set(i);
19 }
20 }
21
22 //檢查然後更新
23 public void setUpper(int i) {
24 if(i >= lower.get()) { //upper依賴lower的值,有可能lower的值已經失效
25 upper.set(i);
26 }
27 }
28
29 public static void main(String[] args) {
30
31
32 NumberRange nr = new NumberRange();
33 while(stopAllThread == false) {
34 for(int i = 0; i < 10000; i++) {
35
36 if(stopAllThread == true)
37 break;
38
39 new Thread(new Runnable() {
40 @Override
41 public void run() {
42
43 if(stopAllThread == true)
44 return;
45
46 if(flag == true)
47 {
48 flag = false;
49 nr.setLower(count++);
50 }
51 else {
52 flag = true;
53 nr.setUpper(count);
54 }
55 if(nr.lower.get() > nr.upper.get()) //檢測到無效狀態,lower > upper
56 {
57 stopAllThread = true;
58 System.out.println("state wrong");//列印錯誤信息
59 System.out.println("lower = " + nr.lower.get() + " upper = " + nr.upper.get());
60 }
61 }
62 }).start();
63 }
64 while(Thread.activeCount() > 1);
65 System.out.println("lower = " + nr.lower.get() + " upper = " + nr.upper.get());
66 }
67 }
68}

在上面的程式中,併發的情況下我們可以檢測到無效狀態,即 upper 的值大於 lower 的值。這便是不滿足我們的不變性條件,因為狀態變數 lower 和 upper 不是彼此獨立的,因此 NumberRange 不能將線程安全委托給他的線程安全狀態變數。輸出如下:

這裡寫圖片描述這裡寫圖片描述

3) 如何安全的發佈底層的狀態變數?
如果一個狀態變數是線程安全的,並且沒有任何不變性條件來約束他的值,在變數操作上也不存在任何不允許的狀態轉換,那麼就可以安全的發佈這個變數。在示例封閉的代碼清單中,SafePoint 是一個可變的且線程安全的類,我們可以安全的發佈它。

現有的線程安全類添加功能

Java 的類庫中,已經包含了很多線程安全的基礎模塊。通常,我們可以直接拿來重用,並不需要重覆造輪子。重用已有的類庫,可以有效降低開發的工作量,開發風險以及維護成本。下麵將講解三種方式來增加新方法,組合方式將是最優的方法。我們應當避免使用前兩種方式,而所用最後一種方式。

通過繼承基類添加功能(擴展類方式)

假設,我們需要對 Vector 擴展,添加一個[若沒有則添加]的操作。我們想到的最直接的方法應該是修改原始類,但是通常是無法做到的,因為我們極有可能沒法訪問或修改類的源代碼。

現在採用另一種方式,通過繼承基類的方式擴展這個類並添加一個新方法 putIfAbsent。如下所示:

 1import java.util.Vector;
2//ThreadSafe
3public class BetterVector<E> extends Vector<E>{
4 public synchronized boolean putIfAbsent(E x) {
5 boolean absent = !contains(x);
6 if(absent)
7 add(x);
8 return absent;
9 }
10}

這樣就可以成功添加一個新的方法。然而,這比直接在基類代碼增加新方法更加脆弱,因為現在的同步策略被分佈到多個源碼文件中。如果底層的類修改了同步策略並選擇不同的鎖來保護,那麼子類將會失效,不能保證線程安全。

客戶端加鎖機制

同樣,來增加一個新方法 putIfAbsent,請看下麵代碼:

 1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class ListHelper<E> {
6
7 public List<E> list = Collections.synchronizedList(new ArrayList<>());
8 //無效的同步鎖
9 public synchronized boolean putIfAbsent(E x) {
10 boolean absent = !list.contains(x);
11 if(absent)
12 list.add(x);
13 return absent;
14 }
15}

這種方式並不能實現線程安全,它的問題在於同步的時候使用了錯誤的鎖。因為 List 本身用的鎖肯定不是 ListHelper 上的鎖,這意味著 putIfAbsent 相對於其他 List 的方法來說並不是同步的。所以看起來同步了實際上卻沒有什麼卵用。

要使這個方法能夠正確同步,必須在客戶端加鎖。即對於使用某個對象 X 的客戶端代碼,使用 X 本身用於保護其狀態的鎖來保護這段客戶代碼。要使用客戶端加鎖,你必須知道對象 X 使用的是哪個鎖。

在 Vector 和同步封裝器的文檔中指出,他們通過使用 Vector 或封裝器容器的內置鎖來支持客戶端加鎖。上面代碼可以改成如下:

 1import java.util.ArrayList;
2import java.util.Collections;
3import java.util.List;
4
5public class ListHelper<E> {
6
7 public List<E> list = Collections.synchronizedList(new ArrayList<>());
8
9 public boolean putIfAbsent(E x) {
10 synchronized(list) {//客戶端加鎖
11 boolean absent = !list.contains(x);
12 if(absent)
13 list.add(x);
14 return absent;
15 }
16 }
17}

客戶端加鎖方式是很脆弱的加鎖方式,意味他將類 C 的加鎖代碼放到與 C 完全無關的其他類中。所以在使用客戶端加鎖時,需要特別小心。

客戶端加鎖機制和擴展類機制有許多共同點,二者都是講派生類的行為與基類的實現耦合在一起,會破壞實現的封裝性和同步策略的封裝性。

組合

相比前面兩種機制,這是一種更好的方法。如下所示,ImprovedList 將 List 的操作委托給底層的 List 對象,然後自己繼承 List 介面的所有方法並對他們加上同步鎖。

 1import java.util.Collection;
2import java.util.Iterator;
3import java.util.List;
4import java.util.ListIterator;
5
6public class ImprovedList<T> implements List<T>{
7
8 private final List<T> list;
9
10 public ImprovedList(List<T> list) {
11 this.list = list;
12 }
13
14 //同步方法
15 public synchronized boolean putIfAbsent(T x) {
16 boolean contains = list.contains(x);
17 if(!contains)
18 list.add(x);
19 return contains;
20 }
21
22 @Override
23 public synchronized boolean add(T arg0) {
24 list.add(arg0);
25 return false;
26 }
27
28 @Override
29 public synchronized void clear() {
30 // TODO Auto-generated method stub
31 list.clear();
32 }
33
34 //按照此同步方式實現其他方法
35
36}

ImprovedList 增加了一層自身的內置鎖,它不用關心底層的 List 是否線程安全或者底層 List 修改了他自己的加鎖實現,ImprovedList 都能構提供一致的加鎖機制來實現線程安全性。當然,加多一層鎖會導致性能損失,但是 ImprovedList 相比前面兩種方式也更加健壯。

上面就是構建安全類的所有內容,希望對你有所幫助,謝謝!!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 通過我的觀察發現hr標簽可以寫/也可以不寫/,如果不寫/那麼就是按照HTML的規範來編寫的,如果寫上,那麼就是按照XHTML規範來編寫的 但是在HTML5中,由於HTML5相容HTML和XHTML所以寫不寫都可以 那麼以後我們在做前端開發時到底寫還是不寫呢? 這個其實非常簡單,只要按照高級開發工具, ...
  • 1.img標簽中的img其實是英文image的縮寫,所以img標簽的作用,就是告訴瀏覽器我們需要顯示一張圖片 2.img標簽格式:<img src=" "> img是標簽名稱,src是屬性 其實img標簽中的src是英文source的縮寫,所以img標簽中的src就是用來告訴img標簽,需要顯示的圖 ...
  • br標簽,如何在html中換行,可以使用br標簽 1.br標簽的作用:換行 2.br標簽的格式:<br> 3.br標簽的註意點: 3.1多個br標簽可以連續使用,使用了多個br標簽就會換多少行 3.2由於HTML的作用就是用來給文本添加語義,而br標簽的語義是不另起一個段落換行,而在企業開發中一般情 ...
  • 我們在之前已經瞭解過,如果想添加一個圖片,需要寫出以下代碼: <img src="logo.png"> 其實想給src屬性賦值有兩種方式: 相對路徑就是每次都從.html文件所在的文件夾開始查找,我們稱之為相對路徑 1.1 同級 同級就是圖片和.html文件存儲在同一個文件夾中 格式: src="Q ...
  • JDBC:(Java database connectivity) 目的:將Java語言和資料庫解耦和,使得一套Java程式能對應不同的資料庫。 方法:sun公司制定了一套連接資料庫的介面(API)。這套API叫做JDBC,JDBC的介面的實現類由資料庫廠家負責編寫,打包成jar包進行發佈,這些ja ...
  • 本節主要闡述如下兩個問題: 1、Dubbo自定義標簽實現。 2、dubbo通過Spring載入配置文件後,是如何觸發註冊中心、服務提供者、服務消費者按照Dubbo的設計執行相關的功能。 ...
  • Java開源生鮮電商平臺-商品價格的設計與架構(源碼可下載) 說明:Java開源生鮮電商平臺-商品價格的設計與架構,主要是對商品的價格進行研究與系統架構. 一、常見的電商價格 市場價(List Price):這個價格僅是用於顯示,用於襯托網站銷售價格的優惠程度; 銷售價(Sales Price):亦 ...
  • Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 2677 Accepted Submission(s): 1208 Problem Descrip ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...