多線程----Immutable VS Mutable (可變與不可變)

Immutable

    Immutable是什麼意思?不變的、不發生改變的意思。在JDK中有很多的類被設計成不可變的,舉個大家經常用到的類java.lang.StringString類被設計成不可變。String所表示的字符串的內容絕對不會發生變化。因此,在多線程的情況下,String類無需進行互斥處理,不用給方法進行synchronized或者lock等操作,進行上鎖、爭搶鎖、解鎖等流程也是有一定性能損耗的。因此,若能合理的利用Immutable,一定對性能的提升有很大幫助。

爲什麼String不可變?

    那麼爲什麼String是不可變的呢?滿足那些條件纔可以變成不可變的類?大家可以打開String類的源碼:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    /**
     * Converts this string to a new character array.
     *
     * @return  a newly allocated character array whose length is the length
     *          of this string and whose contents are initialized to contain
     *          the character sequence represented by this string.
     */
    public char[] toCharArray() {
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
  }

通過上邊的這段代碼你會發現:

1、首先String類本身是被final修飾過的,表明該類無法進行擴展,無法創建子類。因此類中聲明的方法則不會被重寫。
2、String類中的成員變量都被final修飾,同時均爲private,被final修飾則表示成員變量
不會被setter方法再次賦值,private則表示成員變量均爲類私有,外部無法直接調用。
3、String類中的成員變量都沒有setter方法,避免其他接口調用改變成員變量的值。
4、通過構造器初始化所有成員,同時在String中賦值是用的Arrays.copyOf等深拷貝方法。
5、在getter方法中,不要直接返回對象本身,而是克隆對象,並返回對象的拷貝。

以上5點保證String類的不可變性(immutability)。

示例程序

    下面咱們通過一些簡單的示例程序來實驗Immutable,自己動手,豐衣足食,多動手會有好處的。下邊定義三個類。

類名 說明
People 表示一個人的類
PeopleThread 表示People實例的線程的類
Main 測試程序行爲的類

下邊看下每個類的示例代碼,People類,類以及成員變量均被final修飾,同時只能通過構造函數來對成員變量賦值,沒有setter方法:

public final class People {
    private final String sex;
    private final int age;
    private final String address;

    public People(String sex, int age, String address) {
        this.sex = sex;
        this.age = age;
        this.address = address;
    }

    public String getSex() {
        return sex;
    }

    public int getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }

    @Override
    public String toString() {
        return "People{" +
                "sex='" + sex + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

PeopleThread類:

public class PeopleThread extends Thread{

    private People people;

    public PeopleThread(People people){
        this.people = people;
    }

    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName() + " prints " + people);
        }
    }
}

Main類:

public class Main {

    public static void main(String[] args) {
        People people = new People("男",27,"北京");
        new PeopleThread(people).start();
        new PeopleThread(people).start();
        new PeopleThread(people).start();
    }
}

下邊是執行結果:

Thread-0 prints People{sex='男', age=27, address='北京'}
Thread-0 prints People{sex='男', age=27, address='北京'}
Thread-0 prints People{sex='男', age=27, address='北京'}
Thread-0 prints People{sex='男', age=27, address='北京'}
Thread-2 prints People{sex='男', age=27, address='北京'}
Thread-2 prints People{sex='男', age=27, address='北京'}
Thread-2 prints People{sex='男', age=27, address='北京'}
Thread-2 prints People{sex='男', age=27, address='北京'}
Thread-1 prints People{sex='男', age=27, address='北京'}
Thread-1 prints People{sex='男', age=27, address='北京'}
Thread-1 prints People{sex='男', age=27, address='北京'}
Thread-1 prints People{sex='男', age=27, address='北京'}

通過結果就可以發現,無論啓動多少個線程,打印的結果其實都是一樣的。因爲People類本身在實例被創建且字段初始化之後,字段的值就不會再被修改,實例的狀態在初始化之後就不會再發生改變,因此也不需要在進行加鎖、解鎖等操作。因爲想破壞也破壞不了。但是有一點比較難的就是如果確保Immutability,因爲在對類的創建過程中少個final,多個setter等,那麼就無法保證類的Immutability

何時使用呢?

上邊簡單的講解了下Immutable模式,那麼在那些情況下考慮使用Immutability不可變性呢?

實例創建後,狀態不再發生變化時

這個可以參考上邊的示例,一個人的屬性被賦值之後就不會發生改變。這種情況下就可以考慮不可變性來實現。

實例是共享的,且被頻繁訪問時

Immutable模式的優點是“不需要使用synchronized來保護”,這就意味着能夠在不是去安全性和生存性的前提下提高性能。當實例被多個線程共享,且有可能被頻繁訪問時,Immutable模式的優點就會凸顯出來。關於不適用synchronized能提高多少性能?下邊做個實現:

public class NoSynchronized {

    private static final long CALL_COUNT = 1000000000L;

    public static void main(String[] args) {
        trial("NotSynch",CALL_COUNT,new NotSynch());
        trial("Synch",CALL_COUNT,new Synch());
    }

    private static void trial(String msg,long count,Object object){
        System.out.println(msg + ": BEGIN");
        long start_time = System.currentTimeMillis();
         for (long i = 0; i< count; i++){
             object.toString();
         }

         System.out.println(msg + ": END");
         System.out.println("Elapsed time = " + (System.currentTimeMillis() - start_time) + "msec");
    }

    static class NotSynch{
        private final String name = "NotSynch";

        @Override
        public String toString() {
            return "NotSynch{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }

    static class Synch{
        private final String name = "Synch";

        @Override
        public synchronized String toString() {
            return "Synch{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

查看執行結果,差了44倍,這是在完全沒有發生線程衝突的情況下測試的,所以測試的時間就是獲取和釋放實例鎖所花費的時間。當然也跟本地的環境也會對時間差有一定的影響,因此僅供參考:

NotSynch: BEGIN
NotSynch: END
Elapsed time = 693msec

Synch: BEGIN
Synch: END
Elapsed time = 31162msec

哪些情況會破壞不可變性?

  • getter返回的不是不可變的類
    舉個例子,比如變量爲StringBuffer類型:
public final class UserDetail {
    private final StringBuffer detail;
    public UserDetail(String sex,int age,String address){
        this.detail = new StringBuffer(sex + ":" + age + ":" + address);
    }

    public StringBuffer getDetail() {
        return detail;
    }

    @Override
    public String toString() {
        return "UserDetail{" +
                "detail=" + detail +
                '}';
    }


    public static void main(String[] args) {
        UserDetail userDetail = new UserDetail("男",27,"北京");
        //顯示
        System.out.println(userDetail);

        StringBuffer detail = userDetail.getDetail();
        detail.append(":::").append("test");
        //再次顯示
        System.out.println(userDetail);
    }
}

運行結果:
UserDetail{detail=:27:北京}
UserDetail{detail=:27:北京:::test}

get結果之後重新進行append操作,StringBuffer包含修改內部狀態的方法,所以detail字段的內容也是可以被外部修改的。

  • 在一個類中使用了其他的類,其他的類是可變的
    當一個不可變的類中使用了其他的可變類之後,那麼受影響不可變的類也會變成可變的類。

擴展

Java的標準類庫中,有些類也用到了Immutable模式

  • java.lang.String
  • java.math.BigInteger && java.math.BigDecimal
  • java.util.regex.Pattern
  • java.lang.Integer && java.lang.Short等基本數據類型包裝類

在一開始的時候說到了String是不可變的,那麼是否真的不可變呢?有一種方式是可以更改其狀態的,反射機制。

//創建字符串"Hello World", 並賦給引用s
    String s = "Hello World"; 
    System.out.println("s = " + s); //Hello World

    //獲取String類中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    //改變value屬性的訪問權限
    valueFieldOfString.setAccessible(true);

    //獲取s對象上的value屬性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    //改變value所引用的數組中的第5個字符
    value[5] = '#';
    System.out.println("s = " + s);  //Hello#World

運行結果
s = Hello World
s = Hello#World

發現String的值已經發生了改變。也就是說,通過反射是可以修改所謂的“不可變”對象的。這些在使用的時候都是需要注意的地方。

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