1. 程式人生 > >Java併發程式設計(3)-構造執行緒安全類的模式

Java併發程式設計(3)-構造執行緒安全類的模式

文章目錄


更多關於Java併發程式設計的文章請點選這裡:

Java併發程式設計實踐(0)-目錄頁


到目前為止,前兩篇文章已經介紹了執行緒安全與同步的基礎知識。但是我們並不希望為了獲得執行緒安全而去分析每次記憶體訪問;而希望執行緒安全的元件能夠以安全的方式組合成更大的元件或者程式。這一篇將介紹一些構造執行緒安全類的通用模式,這些模式讓類更容易成為執行緒安全類。
本篇總結自《Java併發程式設計實踐》第四章 組合物件 章節的內容 ,詳情可以查閱該書。

一、例項限制模式

即使一個物件不是執行緒安全的,仍然有許多技術可以使它成為安全的多執行緒程式。比如,你可以確保它只被單一的執行緒所訪問(上篇講的執行緒限制技術),也可以確保所有的訪問都正確地被鎖保護。

1.1、 限制變數確保執行緒安全

將資料封裝在物件內部,把對資料的訪問限制在物件的方法上,更容易確保執行緒在訪問資料時總能獲得正確的鎖。如下面的程式:

public class PersonMap{
	//persons是一個有狀態的物件,每個執行緒訪問該變數可能都不同
	private final Map persons = new HashMap<String,Person>();
	
	//為新增Person的方法上鎖
	public synchronized Map addPerson(String personName,Person person){
		return
persons.put(personName,person); } }

可以看出,persons是一個有狀態的變數,有狀態的變數通常是導致類執行緒不安全的因素,可是這個PersonMap類是執行緒安全的,這是因為在這個類中,執行緒需要改變變數的唯一方法是進入addPerson這個方法,但是這個方法是被synchronized關鍵字上鎖的,所以唯一的入口已經安全,那麼整個類都是安全的。
在這裡插入圖片描述

1.2、分析ArrayList的執行緒安全性

ArrayList的主要原始碼:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { /**
     * 列表元素集合陣列
     * 如果新建ArrayList物件時沒有指定大小,那麼會將EMPTY_ELEMENTDATA賦值給elementData,
     * 並在第一次新增元素時,將列表容量設定為DEFAULT_CAPACITY 
     */ transient Object[] elementData; /**
     * 列表大小,elementData中儲存的元素個數
     */ private int size; }

所以通過這兩個欄位我們可以看出,ArrayList的實現主要就是用了一個Object的陣列,用來儲存所有的元素,以及一個size變數用來儲存當前陣列中已經添加了多少元素。其中的elementData和size兩個變數是有狀態變數,且沒有任何的處理,接下來看看add方法:

public boolean add(E e) { /**
     * 新增一個元素時,做了如下兩步操作
     * 1.判斷列表的capacity容量是否足夠,是否需要擴容
     * 2.真正將元素放在列表的元素數組裡面
     */ ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }

當2個執行緒共同進入add方法時,例如當前size為5,那麼執行緒1訪問到size為5時,ensureCapacityInternal()方法對size進行擴容,將size擴容為6;在這個時候執行緒2讀取到的size是6,已經可以插入資料,但是這個資料已經被執行緒1插入了,所以產生了執行緒的不安全。那應該怎麼解決呢?我們可以限制變數確保執行緒安全,如下列程式碼:

public synchronized boolean add(E e) { /**
     * 新增一個元素時,做了如下兩步操作
     * 1.判斷列表的capacity容量是否足夠,是否需要擴容
     * 2.真正將元素放在列表的元素數組裡面
     */ ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }

1.3、總結

限制性使構造執行緒安全的類變得更容易。因為類的狀態被限制後,分析它的執行緒安全性時,就不必檢查完整的查詢。

二、委託執行緒安全模式

2.1、什麼是委託執行緒安全

我們還可以將執行緒的安全性委託給多個狀態變數,只要這些變數是彼此獨立的,即組合而成的類並不會在其包含的多個狀態變數上增加任何不變性條件。

2.2、委託執行緒安全的例項

public class StatefulServlet extends HttpServlet{
    //使用concurent併發包中的atomic工具包下的原子變數類,保證多執行緒請求下的原子操作
    private AtomicLong count = new AtomicLong(0);
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String paramName = servletRequest.getParameter("paramName");
        count.incrementAndGet();//自增1
        servletResponse.getWriter().write(paramName+count);
    }
}

這就是個很好的委託執行緒安全模式,即把整個類的執行緒安全狀況委託給了這個count變數,由於count變數採用的了原子變數類,所以這個委託是生效的,該類也執行緒安全。

三、基於執行緒安全類的擴充套件模式

Java類庫中包含了許多有用的“構建塊”類。重用這些已有的類要好與建立一個新的類。重用能夠降低開發的難度、風險以及維護成本。有時一個執行緒安全類支援我們需要的全部操作,但更多的時候,一個類只支援我們需要的大部分操作,這時需要在不破壞其執行緒安全性的前提下,向它新增一個新的操作。這就是基於執行緒安全類的擴充套件模式。

3.1、基於Vector的功能擴充套件

新增一個新原子操作的最安全的方式是,修改原始的類,以支援期望的操作。但是你可能無法訪問原始碼或者修改的自由,所以通常需要擴充套件類。

public class BetterVector<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
	boolean absent = !contains(x);
		If(absent){
			add(x);
		}
		return absent;
}
}

3.2、基於List的組合

向已有的類中新增一個原子操作,還有更健壯的選擇:組合。下列程式碼中,ImprovedList通過將操作委託給底層的List例項,實現了List的操作,同時還添加了一個原子的putIfAbsent方法。

public class ImproveList<T> extends List<T>{
	private final List<T> list;
	
	public ImproveList(List<T> list){
		this.list = list;
	}
	
	public synchronized boolean addIfAbsent(T x){
		if(contains){
			list.add(x);
			return true;
		}
		return false;
	}
	
}

在這裡插入圖片描述
通過內部鎖,ImproveLis引入了一個新的鎖層。它並不關係底層的List是否執行緒安全,即使List不是執行緒安全的,雖然額外的一層同步可能會帶來一些微弱的效能損失,但是對於併發程式來說,應該永遠堅持:寧可犧牲時間,也要保證安全!