Java多線程--設計模式(二、Immutable Object(不可變對象)模式)

一、Immutable Object 模式簡介

多線程共享變量的情況下,爲了保證數據的一致性,往往需要對這些變量的訪問進行加鎖。而鎖本身又會帶來一些問題和開銷。Immutable Object 模式使得我們可以在不使用鎖的情況下,既保證共享變量訪問的線程安全,又能避免引入鎖可能帶來的問題和開銷。
這裏採用的方法是狀態不可變,從而保證了數據的一致性,又避免了同步訪問控制所產生的額外開銷和問題,也簡化了編程。
下例2.1展示了非線程安全狀態的售票站模型:

public class Ticket{
	
	private int ticket;

	public Ticket(int ticket){
		this.ticket = ticket;
	}

	public int getTicket(){
		return ticket;
	}

	public void setTicket(int ticket){
		this.ticket = ticket;
	}
}

當系統更新票數時,需要調用 setTicket 方法來更新,顯然,這是非線程安全的,因爲對票數的寫操作不是一個原子操作。這時我們可以將門票建模爲狀態不可變的對象。
如下例2.2:

public final class TIcket{
	public final int ticket;

	public Ticket(int ticket){
		this.ticket = ticket;
	}
}

使用狀態不可變的門票模型時,如果票數發生改動,則通過替換整個門票對象來實現的。

因此,所謂狀態不可變的對象並非指被建模的現實世界實體的狀態不可變,而是我們在建模的時候的一種決策;現實世界實體的狀態總是在變化的,但我們可以用狀態不可變的對象來對這些實體進行建模。

二、Immutable Object 模式的架構

Immutable Object 模式將現實世界中狀態可變的實體建模爲狀態不可變對象,並通過創建不同的狀態不可變的對象來反映實現世界實體的狀態變更。

圖2.1展示了該模式的幾個主要參與者:
在這裏插入圖片描述

  • ImmutableObject:負責存儲一組不可變狀態。該參與者不對外暴露任何可以修改其狀態的方法,其主要方法及職責如下:
    1. getStateX, getStateY:這些 getter方法返回其所屬 ImmutableObject 實例所維護的狀態相關變量的值。這些變量在對象實例化時通過其構造器的參數獲得值。
    2. getStateSnapshot:負責維護 ImmutableObject 所建模的現實世界實體狀態的改變。當相應的現實實體狀態改變時,該參與者負責生成新的 ImmutableObject 的實例,以反映新的狀態。
  • Manipulator:負責維護 ImmutableObject 所建模的現實世界實體狀態的變更。當相應的現實實體狀態變更時,該參與者負責生成新的 ImmutableObject 的實例,以反映新的狀態。
    1. changeStateTo:根據新的狀態值生成新的 ImmutableObject 的實例。
  • 獲取單個狀態的值:調用不可變對象的相關 getter 方法即可實現。
  • 獲取一組狀態的快照:不可變對象可以提供一個 getter 方法,該方法需要對其返回值做防禦性複製或者返回一個只讀對象,以避免其狀態對外泄露而被改變。
  • 生成新的不可變對象實例:當建模對象的狀態發生改變時,創建新的不可變對象實例來反映這種變化。

圖2.2展示了該模式典型交互場景的序列圖:
在這裏插入圖片描述
注意,一個嚴格意義上不可變對象要滿足以下所有條件:

  1. 類本身使用 final 修飾:防止其子類改變其定義的行爲。
  2. 所有字段都是用final修飾的:final 修飾的字段在其他線程可見時,它必定是初始化完成的,而非 final 修飾的字段由於缺少這種保證,可能導致一個線程“看到”一個字段的時候,它還沒有被初始化完成,從而導致一些不可預料的後果。
  3. 在對象的創建過程中,this 關鍵字沒有泄露給其他類:防止其他類(如該類的內部匿名類)在對象創建的過程中修改其狀態。
  4. 任何字段,若其引用了其他狀態可變的對象(如集合、數組等),這這些字段必須是 private 修飾的,並且這些字段值不能對外暴露。若有相關方法要返回這些字段值,應該進行防禦性複製(Defensive Copy)。

三、Immutable Object 模式案例分析

在多線程的情況下,模擬一個線程安全的賣票系統。

創建一個門票信息的類

/**
 * 門票信息
 *
 * 模式角色: ImmutableObject.ImmutableObject
 */
public final class TicInfo {

    /**
     * 剩餘門票數量
     */
    private final int ticNum;

    public TicInfo(int ticNum) {
        this.ticNum = ticNum;
    }

    public int getTicNum() {
        return ticNum;
    }
}

用線程來處理門票售賣

/**
 * 變更門票信息
 *
 * 模式角色:ImmutableObject.Manipulator
 */

public class TicChange implements Runnable{
    private final TicInfo test;

    public TicChange(TicInfo test) {
        this.test = test;
    }

    @Override
    public void run() {
        System.out.println("剩餘門票數爲:" + test.getTicNum() + "張,已成功爲您購買!");
    }
}

創建一個測試類

public class Test {
    public static void main(String[] args){

        /**
         * 給定初始門票數
         */
        final int ticNum = 100;

        for (int i = ticNum;i > 0;i--){
            TicInfo test = new TicInfo(i);
            TicChange thread = new TicChange(test);
            new Thread(thread).start();
        }
    }
}

結果預覽:
在這裏插入圖片描述

四、Immutable Object 模式的評價與實現考量

不可變對象具有天生的安全性,多個線程共享一個不可變對象的時候無須使用額外的併發訪問控制,這使得我們可以避免顯式鎖等併發訪問控制的開銷和問題,簡化了多線程編程。

Immutable Object 模式特別適用於以下場景

  • 被建模對象的狀態變化不頻繁
  • 同時對一組相關的數據進行寫操作,因此需要保證原子性
    此場景爲了保證操作的原子性,通常的做法是使用顯式鎖。但若採用 Immutable Object 模式,將這一組相關的數據“組合”成一個不可變對象,則對這一組數據的操作就可以無需加顯式鎖也能保證原子性,既簡化了編程,又提高了代碼運行效率。
  • 使用某個對象作爲安全的 HashMap 的 Key
    我們知道,一個對象作爲 HashMap 的 Key 被“放入 HashMap 之後,若該對象狀態變化導致了其 Hash Code 的變化,則會導致後面在用同樣的對象作爲 Key 去 get 的時候無法獲取關聯的值,儘管該 HashMap 中的確存在以該對象爲 Key 的條目。相反,由於不可變對象的狀態不變,因此其 Hash Code 也不變。這使得不可變對象非常適於用作 HashMap 的 Key 。

Immutable Object 模式實現時需要注意以下幾個問題

  • 被建模對象的狀態變更比較頻繁
    此時也不見得不能使用 Immutable Object 模式。只是這意味着頻繁創建新的不可變對象,因此會增加GC(Garbage Collection)的負擔和CPU消耗,我們需要綜合考慮:被建模對象的規模、代碼目標運行環境的JVM內存分配情況、系統對吞吐率和響應性的要求。若這幾個方面因素綜合考慮都能滿足要求,那麼使用不可變對象建模也未嘗不可。
  • 使用等效或者近似的不可變對象
    有時創建嚴格意義上的不可變對象比較難,但是儘量向嚴格意義上的不可變對象靠攏也有利於發揮不可變對象的好處。
  • 防禦性複製
    如果不可變對象本身包含一些狀態需要對外暴露,而相應的字段本身又是可變的(如 HashMap),那麼在返回這些字段的方法還是需要做防禦性拷貝,以避免外部代碼修改了其內部狀態。

總結

本文介紹了Immutable Object模式的意圖及架構。並結合筆者經驗提供了一個實際的案例用於展示使用該模式的典型場景,在此基礎上對該模式進行了評價並分享在實際運用該模式時需要注意的事項。

參考資源

[1]黃文海. Java多線程編程指南.北京:電子工業出版社,2015.10.
[2]衝鍋煮酒. Immutable Object模式 - 多線程. https://www.cnblogs.com/pingyun/p/11451078.html.
[3]Brian Goetz. Java theory and practice:To mutate or not to mutate?. http://www.ibm.com/developworks/java/library/j-jtp02183/index.html.
[4]Brian Goetz et al. Java Concurrency In Practice. Addison Wesley, 2006.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章