減少內存的佔用問題——享元模式和單例模式的對比分析

前言

接口的經常使用用法都有什麼?策略模式複習總結 的話題提起了:如何解決策略類膨脹的問題,說到html

「有時候能夠經過把依賴於環境Context類的狀態保存到客戶端裏面,而將策略類設計成可共享的,這樣策略類實例能夠被不一樣客戶端使用。」java

換言之,可使用享元模式來減小對象的數量,享元模式它的英文名字叫 Flyweigh 模式,翻譯爲羽量級(搏擊比賽的術語,也就是輕量級的體現)模式,它是構造型模式之一,它經過與其餘相似對象共享數據來減少內存佔用,也正應了它的名字:享-分享。數據庫

那麼享元模式究竟是什麼樣子的呢?下面看個例子,有一個文檔,裏面寫了不少英文,你們知道英文字母有26個,大小寫一塊兒一共是52個:設計模式

保存這個文件的時候,全部單詞都佔據了一分內存,每一個字母都是一個對象,若是文檔裏的字母有重複的,怎麼辦?數組

難道每次都要建立新的字母對象去保存麼?答案是否認的,其實每一個字母只須要建立一次,而後把他們保存起來,當再次使用的時候直接在已經建立好的字母裏取就ok了,這就是享元模式的一個思想的體現。緩存

說到這兒,其實想起了Java的String類,這個類就是應用了享元模式安全

享元模式的樣子

先看享元模式的具體實現例子:性能優化

抽象享元角色(接口或者抽象類):全部具體享元類的父類,規定一些須要實現的公共接口session

具體享元角色:抽象享元角色的具體實現類,並實現了抽象享元角色規定的方法多線程

享元工廠角色:負責建立和管理享元角色。它必須保證享元對象能夠被系統適當地共享。當一個客戶端對象調用一個享元對象的時候,享元工廠角色會檢查系統中是否已經有一個符合要求的享元對象。若是已經有了,享元工廠角色就應當提供這個已有的享元對象,若是系統中沒有一個適當的享元對象的話,享元工廠角色就應當建立一個合適的享元對象。

生成字符的例子

代碼以下:

public interface ICharacter {
    /**
     * 享元模式的抽象享元角色,全部具體享元類的父類,規定一些須要實現的公共接口。其實沒有這個接口也能夠的。
     * 顯式我本身的字母
     */
    void displayCharacter();
}

/**
 * 具體的享元模式角色
 */
public class ChracterBuilder implements ICharacter {
    private char aChar;

    public ChracterBuilder(char c) {
        this.aChar = c;
    }

    @Override
    public void displayCharacter() {
        System.out.println(aChar);
    }
}

// 享元工廠類
public class FlyWeightFactory {
    /**
     * 享元工廠裏維護一個內存的「共享池」,來避免大量相同內容對象的開銷。這種開銷最多見、最直觀的就是內存的損耗。
     * 咱們這裏使用數組也行,或者 HashMap(concurrenthashmap等)
     */
    private Map<Character, ICharacter> characterPool;

    public FlyWeightFactory() {
        this.characterPool = new HashMap<>();
    }

    public ICharacter getICharater(Character character) {
        // 建立(得到)一個對象的時候,先去 pool 裏判斷,是否已經存在
        ICharacter iCharacter = this.characterPool.get(character);
        if (iCharacter == null) {
            // 若是共享池裏沒有,才 new 一個新的,並同時加到 pool 裏,緩存起來
            iCharacter = new ChracterBuilder(character);
            this.characterPool.put(character, iCharacter);
        }

        // 不然,直接從pool裏取出,再也不新建
        return iCharacter;
    }
}

客戶端調用,下面的客戶端代碼其實不是享元模式的真正體現,只是普通的調用,爲了對比說明問題:

public class MainFlyWeight {
    public static void main(String[] args) {
        // 不用享元模式,咱們每次建立相同內容的字母的時候,都要new一個新的對象
        ICharacter iCharacter = new ChracterBuilder('a');
        ICharacter iCharacter1 = new ChracterBuilder('b');
        ICharacter iCharacter2 = new ChracterBuilder('b');
        ICharacter iCharacter3 = new ChracterBuilder('b');
        ICharacter iCharacter4 = new ChracterBuilder('b');

        iCharacter.displayCharacter();
        iCharacter1.displayCharacter();
        iCharacter2.displayCharacter();
        iCharacter3.displayCharacter();
        iCharacter4.displayCharacter();

        if (iCharacter2 == iCharacter1) {
            System.out.print("true");
        } else {
            // 執行後,發現打印了 false,說明是兩個不一樣的對象
            System.out.print("false");
        }
}

下面使用享元模式,必須指出的是,使用享元模式,那麼客戶端絕對不能夠直接將具體享元類實例化,而必須經過工廠獲得享元對象

public class MainFlyWeight {
    public static void main(String[] args) {
        // 使用享元模式,必須指出的是,客戶端不能夠直接將具體享元類實例化,而必須經過一個工廠
        FlyWeightFactory flyWeightFactory = new FlyWeightFactory();
        ICharacter iCharacter = flyWeightFactory.getICharater('a');
        ICharacter iCharacter1 = flyWeightFactory.getICharater('b');
        ICharacter iCharacter2 = flyWeightFactory.getICharater('b');

        iCharacter.displayCharacter();
        iCharacter1.displayCharacter();
        iCharacter2.displayCharacter();

        if (iCharacter1 == iCharacter2) {
            // 確實打印了 =============,說明對象共享了
            System.out.print("============");
        }
    }
}

打印的都同樣,可是對象使用的內存卻不同了,減小了內存的佔用。

類圖以下:

通常而言,享元工廠對象在整個系統中只有一個,所以也可使用單例模式,由工廠方法產生所須要的享元對象且設計模式不用拘泥於具體代碼, 代碼實現可能有n多種方式,n多語言……

教師管理的例子

再看一例子,需求很簡單。有一個老師類,繼承 Person 類,老師類裏保存一個數字編號,客戶端能夠經過它來找到對應的老師。

爲了節省篇幅,簡單的堆砌一個 name 信息便可。

其中享元工廠類設計爲單例的。

public class Person {
    private String name;/**
     * person是享元抽象角色
     */
    public Person(String name) {this.name = name;
    }

    public Person() {
    }public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

// 具體享元角色
public class Teacher extends Person {
    private int number;

    public Teacher(int number, String name) {
        super(name);
        this.number = number;
    }

    public Teacher() {
        super();
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

// 享元工廠類,設計爲單例的
public class TeacherFactory {
    private Map<Integer, Teacher> integerTeacherMapPool;

    private TeacherFactory() {
        this.integerTeacherMapPool = new HashMap<>();
    }

    public static TeacherFactory getInstance() {
        return Holder.instance;
    }

    public Teacher getTeacher(int num) {
        Teacher teacher = integerTeacherMapPool.get(num);
        if (teacher == null) {
            // TODO 模擬用,不要把 teacher 寫死,每次使用 set
            teacher = new Teacher();
            teacher.setNumber(num);
            integerTeacherMapPool.put(num, teacher);
        }

        return teacher;
    }

    private static class Holder {
        private static final TeacherFactory instance = new TeacherFactory();
    }
}
 
/////// 客戶端,查詢老師
public class MainClass {
    public static void main(String[] args) {
        // 先建立工廠
        TeacherFactory teacherFactory = TeacherFactory.getInstance();
        // 經過工廠獲得具體老師對象
        Teacher teacher = teacherFactory.getTeacher(1000);
        Teacher teacher1 = teacherFactory.getTeacher(1001);
        Teacher teacher2 = teacherFactory.getTeacher(1000);

        System.out.println(teacher.getNumber());
        System.out.println(teacher1.getNumber());
        System.out.println(teacher2.getNumber());

        // 判斷是不是相等對象
        if (teacher == teacher2) {
            // 確實打印了,ok
            System.out.print("____________-");
        }
    }
}

享元模式的使用場景

一、應用程序的底層性能優化時經常使用的一種策略

好比,一個系統中存在着大量的細粒度對象,且這些細粒度對象耗費了大量的內存。這裏也要明白,享元模式比起工廠,單例,策略,裝飾,觀察者等模式,其實不算是經常使用的設計模式,它主要用在底層的設計上比較多,好比 JDK 的 String 類,Integer 的 valueOf(int)方法等。

二、管理大量類似對象的一種策略,能夠理解爲緩存的實現方案

其實感受1和2是一個意思。。。

三、大量的細粒度對象的狀態中的大部分均可之外部化

理解對象的內部狀態和外部狀態兩個概念

如今多了幾個新的概念(外部化,內部,外部狀態……):

內部狀態:存儲在享元對象內部的對象(能夠理解爲其內部的一些穩定的屬性),這些屬性對象是不會隨環境的改變而變化的。正由於這個緣由,一個享元對象才能夠被共享

外部狀態:對象的一些屬性會隨環境的改變而改變,屬於不穩定的屬性,故這些屬性對象不能夠被共享

由此獲得一個結論:享元對象的外部狀態必須由客戶端保存,並在享元對象被建立以後,在須要使用的時候再傳入到享元對象內部。外部狀態不能夠影響享元對象的內部狀態,它們是相互獨立的

知足以上的這些條件的系統纔可使用享元模式。

回憶以前的教師管理例子:具體享元角色類Teacher類的int類型的number屬性,其實就是一個內部狀態,它的值會在享元對象被建立時賦予,也就是所謂的內部狀態對象讓享元對象本身去保存,且能夠被客戶端共享,全部的內部狀態在對象建立以後,就再也不改變。

具備外部狀態的享元模式實現——模擬數據庫鏈接池小例子

這個教師管理的例子,其享元對象沒有外部狀態,下面看一個具備外部狀態+內部狀態的享元模式例子——常見的一些數據庫鏈接池,其實就是利用享元模式對數據庫鏈接進行封裝和共享。

若是一個享元對象有外部狀態,全部的外部狀態都必須存儲在客戶端,在使用享元對象時,再由客戶端傳入享元對象。

public interface BaseDao {
    /**
     * 鏈接數據源,享元模式的抽象享元角色
     * @param session String 數據源鏈接的session,該參數就是外部狀態
     */
    void connect(String session);
}

例子裏只有一個外部狀態——connect()方法的參數 session,實際上這個 session 是很複雜的,咱們這裏簡單的用 string 代替。

外部狀態:對象的一些屬性會隨環境的改變而改變,屬於不穩定的屬性,故這些屬性對象不能夠被共享

享元對象的外部狀態必須由客戶端保存,並在享元對象被建立以後,在須要使用的時候再傳入到享元對象內部。

外部狀態不能夠影響享元對象的內部狀態,它們是相互獨立的

public class DaoA implements BaseDao {
    /**
     * 內部狀態,存儲每一個數據庫鏈接的一些信息,這裏也用字符串簡化了
     */
    private String strConn = null;

    /**
     * 內部狀態在建立享元對象的時候做爲參數傳入構造器
     */
    public DaoA(String s) {
        this.strConn = s;
    }

    /**
     * 外部狀態 session 做爲參數傳入抽象方法,能夠改變方法的行爲,可是對於內部狀態不作改變,二者獨立
     * 外部狀態(對象)存儲在客戶端,當客戶端使用享元對象的時候才被傳入享元對象,而不是開始就有。*/
    @Override
    public void connect(String session) {
        System.out.print("內部狀態 是" + this.strConn);
        System.out.print("外部狀態 是" + session);
    }
}

享元工廠

public enum Factory {
    /**
     * 單例模式的最佳實現是使用枚舉類型。只須要編寫一個包含單個元素的枚舉類型便可
     * 簡潔,且無償提供序列化,並由JVM從根本上提供線程安全的保障,絕對防止屢次實例化,且可以抵禦反射和序列化的攻擊。
     */
    instance;

    /**
     * 能夠有本身的操做
     */
    private Map<String, BaseDao> stringBaseDaoMapPool = new HashMap<>();

    public BaseDao factory(String s) {
        BaseDao baseDao = this.stringBaseDaoMapPool.get(s);
        if (baseDao == null) {
            baseDao = new DaoA(s);
            this.stringBaseDaoMapPool.put(s, baseDao);
        }

        return baseDao;
    }
}

下面是客戶端調用,雖然客戶端申請了三個享元對象,可是實際建立的享元對象只有兩個,這就是共享的含義

public class Client {
    public static void main(String[] args) {
        BaseDao baseDao = Factory.instance.factory("A鏈接數據源");
        BaseDao baseDao1 = Factory.instance.factory("B鏈接數據源");
        BaseDao baseDao2 = Factory.instance.factory("A鏈接數據源");
        baseDao.connect("session1");
        baseDao1.connect("session2");
        baseDao2.connect("session1");

        if (baseDao == baseDao2) {
            // 確實打印了
            System.out.print("===========");
        }
    }
}

享元模式的優勢和缺陷

享元模式的優勢在於,它能大幅度地下降內存中對象的數量。可是,它作到這一點所付出的代價也是很高的:

一、享元模式使得系統更加複雜。

爲了使對象能夠共享,須要將一些狀態外部化,這使得程序的邏輯複雜化

二、享元模式將享元對象的狀態外部化,而讀取外部狀態使得運行時間稍微變長

三、享元模式須要維護一個記錄了系統已有的全部享元的哈希表,也稱之爲對象池,這也須要耗費必定的資源。應當在有足夠多的享元實例可供共享時才值得使用享元模式。

單例模式和享元模式的比較

享元模式到這裏總結的差很少了,前面的享元模式的例子,工廠Factory類使用了單例模式實現,那麼這裏還要順便總結一個老生常談,可是又不見得真的談對了的設計模式——單例模式。

具體細節:最簡單的設計模式——單例模式的演進和推薦寫法(Java 版)

下面是簡單分析:

享元是對象級別的:在多個使用到這個對象的地方都只須要使用這一個對象便可知足要求。

單例是類級別的:這個類必須只能實例化出一個對象。

能夠這麼說:單例是享元的一種特例。設計模式不用拘泥於具體代碼, 代碼實現可能有 n 多種方式,而單例能夠看作是享元的實現方式中的一種,只不過他比享元更加嚴格的控制了對象的惟一性。

享元模式和線程安全

前面的例子都是使用的 hashmap,做爲對象池,若是在多線程的場景下,是否安全呢?

答案是否認的,必須給工廠裏的 getxxx 方法加鎖,好比直接使用 synchronized 關鍵字便可。看下面例子:

public class TeacherFactory {
    private Map<Integer, Teacher> integerTeacherMapPool;

    private TeacherFactory() {
        this.integerTeacherMapPool = new HashMap<>();
    }

    public static TeacherFactory getInstance() {
        return Holder.instance;
    }

    public synchronized Teacher getTeacher(int num) {
        Teacher teacher = integerTeacherMapPool.get(num);
        if (teacher == null) {
            // TODO 模擬用,不要把 teacher 寫死,每次使用 set
            teacher = new Teacher();
            teacher.setNumber(num);
            integerTeacherMapPool.put(num, teacher);
        }

        return teacher;
    }

    private static class Holder {
        private static final TeacherFactory instance = new TeacherFactory();
    }
}

若是不加鎖,可能有以下場景:

線程1 線程2
執行 getTeacher(100) 方法  
判斷拿出的對象是否爲 null  
爲 null,new Teacher  
  執行 getTeacher(100) 方法
  判斷拿出的對象是否爲 null
  爲 null,new Teacher
  put 到對象池
put 到對象池  

 

 

  

 

 

 

 

 

 

固然,也能夠直接使用 concurrentHashMap

在JDK中有哪些使用享元模式的例子?舉例說明。

說兩個,一個是String類,第二個是java.lang.Integer 的 valueOf(int)方法。

String 類

針對String,也是老生常談了,它是final的,字符串常量一般是在編譯的時候就肯定好的,定義在類的方法區裏。以下:

String s1 = "hello";
String s2 = "he" + "llo";

if (s1 == s2) {
    System.out.print("====");// 打印了,說明 s1,s2 引用了同一個對象 hello
}

使用相同的字符序列,而不是使用 new 關鍵字建立的兩個字符串,會建立指向Java字符串常量池中的同一個字符串的指針。字符串常量池是 Java 節約資源的一種方式,其實就是使用了享元模式的思想。

字符串的分配,和其餘的對象分配同樣,耗費高昂的時間與空間代價,JVM 爲了提升性能和減小內存開銷,在實例化字符串常量的時候進行了一些優化。

字符串類維護了一個字符串池,每當代碼建立字符串常量時,JVM 會首先檢查字符串常量池,若是字符串已經在池中,就返回池中的實例的引用;若是字符串不在池中,就會實例化一個字符串並放到池中,Java可以進行這樣的優化是由於字符串是不可變的,能夠不用擔憂數據衝突

java.lang.Integer 的 valueOf(int)方法源碼分析(8.0版本)

先看一個例子A

Integer a = 1;
Integer b = 1;
System.out.print(a == b); // true

再看一例子B

// 再看一例子;
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.print(a == b); // false

如上比較的結果很容易理解,再看一個「奇怪的」例子C

Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false

比較的結果怎麼仍是 false 呢? 例子 A 裏明明是 true,爲何到了例子 C 裏就是 false 了?

反編譯上述程序,看看發生了什麼

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 19 L0
    SIPUSH 200
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 發現每次都是使用了自動裝箱
    ASTORE 1
   L1
    LINENUMBER 20 L1
    SIPUSH 200
    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
    ASTORE 2

發現每次賦值的時候,都會自動調用其自動裝箱方法,以下

Integer c = Integer.valueOf(200);

再看該方法源碼

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

發現,當使用Integer的自動裝箱時,i 值在 cache 的 low 和 high 之間時,會用緩存保存起來,供客戶端屢次使用,以節約內存。若是不在這個範圍內,則建立一個新的 Integer 對象,這就是享元模式的設計思想。

看看範圍:-128~+127

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
View Code
相關文章
相關標籤/搜索