[-設計模式知幾何-] 建立型05-單例模式

打算開始打理個人公衆號了,最新內容和獨家祕籍。走過路過來捧個場,關注公衆號:編程之王java

本文收錄於公衆號編程之王: 文章內存地址sjms-o-05
如何獲取更多知識乾糧,詳見 <<編程之王食用規範1.0>>android


1.介紹與思考

單例模式:保證一個類僅有一個實例,並提供一個訪問他的全局訪問點編程


1.1:單例與設計原則

單例模式像一個奇葩,和設計原則格格不入。如今對象的建立,所以無接口拓展可言
依賴倒置原則接口隔離原則迪米特原則里氏替換原則合成複用原則研究無從談起。
單例沒法派生本身的族系,全部修改都要在本體中進行,違反開放封閉原則
單例是典型的大包乾,功能的集聚地,可能存在職責太重,而違反單一職責原則設計模式

  • 既然單例徹底不遵照七大設計原則,它爲什麼能在設計模式中立足?

設計原則旨在協調一個軟件實體(類、模塊、函數)之間的結構關係 。
而單例每每只是一個類,沒有本身的族系和朋友圈,它就像孤獨而至高的
其次是由於它真的很是簡單和好用。沒有抽象的族系拓展,讓它能夠很容易被理解。安全


1.2:單例優點與劣勢
---->[優點]----
[1].全局內存中只需有一個實例對象,減少內存開銷
[2].使用一個對象提供訪問,避免對稀缺資源的多重佔用
[3].私有化構造,提供全局的惟一訪問點,嚴格控制訪問

---->[劣勢]----
[1].無接口拓展可言,全部修改都要在本體中進行
[2].可能存在職責太重,而違反單一職責原則
複製代碼

1.3:本文例子

若是上線一個世界程序,一個World對象佔據內存10G
世界不能隨便去new,如何不讓上層沒法主動建立World對象,
World對象佔據內存太大,服務器沒法支撐多個世界對象,須要提供惟一World對象bash

  • 關於單例的幾個要點:
[1].私有構造:將類的構造私有化,從而限制外界訪問。
[2].延遲加載:當且僅當第一次獲取單例對象是纔會建立對象。
[3].線程安全:多線程時不會建立多個該類對象。
[4].防反序列化:反序列化不會建立多個該類對象。
[5].防反射:反射不會建立多個該類對象。
複製代碼

1、單例的n種形式--形式上的一切都僅是開始而已

1.極簡單例(餓漢)

做爲靜態變量直接建立,最大的缺點是單例對象爲沒有延遲加載性服務器

public class World {
    private final static World sWorld = new World();
    //[1]私有化構造
    private World() {
        initWorld();//初始化世界
        System.out.println("世界已建立");
    }
    private void initWorld() {
    }
    //[2]返回內部靜態實例
    public static World getInstance() {
        return sWorld;
    }
}
複製代碼

2.單線程懶加載(懶漢)

最大的缺點是線程不安全,怎麼個不安全法,且聽我細細道來。微信

public class World {
    private static World sWorld = null;
    //[1]私有化構造
    private World() {
        initWorld();//初始化世界
        System.out.println("世界已建立");
    }
    private void initWorld() {

    }
    //[2]返回內部靜態實例
    public static World getInstance() {
        if (sWorld==null){
            sWorld=new World(); 
        }
        return sWorld;
    }
}
複製代碼

之因此稱爲單例,是由於在屢次調用getInstance獲取實例時是相同實例,且構造只執行一次網絡

public class Client {

    public static void main(String[] args) {
        World world = World.getInstance();
        World world2 = World.getInstance();
        World world3 = World.getInstance();

        System.out.println(world);//World@41cf53f9
        System.out.println(world2);//World@41cf53f9
        System.out.println(world3);//World@41cf53f9

    }
}
複製代碼

之因此說線程不安全,由於多線程下sWorld==null可能被屢次經過,因此實例化多個對象。
演示一下,在一個Machine的Runnable對象中調用了World.getInstance()來獲取World對象多線程

public class Machine implements Runnable {
    public void run() {
        World.getInstance();
    }
}
複製代碼

這時在Client中建立1000個線程去使用這個World,千人同時在線,每一個用戶一個訪問線程
若是不做線程安全處理,就會建立多個世界,若是一個世界的渲染須要10G內存,結果可想而知,這樣單例就沒有意義了。

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Machine()).start();
        }
    }
}
複製代碼

若是你會多線程調試,能夠本身干預一下線程的執行。


3.懶漢雙檢鎖

第一檢--該對象是否非空,爲空才進行同步鎖定
第二檢--該對象是否非空,爲空才建立實例

public class World {
    private volatile static World sWorld;
    //[1]私有化構造
    private World() {
        initWorld();//初始化世界
        System.out.println("世界已建立");
    }
    private void initWorld() {
    
    }
    //[2]返回內部靜態實例
    public static World getInstance() {
        if (sWorld == null) {//判斷非空後--執行
            synchronized (World.class) {//加鎖,保證多線程下的單例
                if (sWorld == null) {//非空,建立實例
                    sWorld = new World();
                }
            }
        }
        return sWorld;
    }
}
複製代碼

這樣不管多少個線程World都只會建立一次。雖然synchronized同步會影響一丟丟性能
不過進行了雙檢,只要有sWorld被建立了,是不會走同步的,測試了一下10000000個線程經過第一檢的也就10幾個,因此這樣挺完美的。


  • 關於指令重排序

一些時候指令重排序會將2和3步驟調換來提升性能。但並不是百分百都會重排序。
這在單線程中並無什麼威脅,但這裏多線程中sWorld == null
若是發生重排序,sWorld指向內存空間,就會非空,若是實例化尚未來及。
下一個線程進入就會獲取到一個未初始化完成的對象,在使用它時會空指針異常。
解決方案很簡單在實例聲明時加上volatile關鍵字便可。


4.靜態內部類

原理:Class對象的初始化鎖。和上面的功能基本,因此我喜歡這個

public class World {
    //[1]私有化構造
    private World() {
        initWorld();//初始化世界
        System.out.println("世界已建立");
    }
    private void initWorld() {
    }
    //[3]返回內部靜態實例
    public static World getInstance() {
        return WorldHolder.sWorld;
    }
    //[2]建立內部類建立實例
    private static class WorldHolder {
        private static final World sWorld = new World();
    }
}
複製代碼

5.至簡--枚舉

枚舉默認私有化構造器,防反射,防反序列化。

public enum World {
    INSTANCE;
    World() {
        initWorld();//初始化世界
        System.out.println("世界已建立");
    }
    private void initWorld() {
    }
}
複製代碼

關於枚舉:下面是經過jad反編譯獲得的枚舉源碼,可見枚舉在JVM的眼中也只是一個類而已,
而且私有化構造+靜態代碼塊初始實例,自然的單例材料。因爲靜態代碼塊初始實例,因此不是懶加載
命令:jad -s .java -8 World.class

package com.toly1994.dp.creational.singleton.world.enum_;

import java.io.PrintStream;
public final class World extends Enum{
    public static World[] values(){
        return (World[])$VALUES.clone();
    }
    
    public static World valueOf(String name){
        return (World)Enum.valueOf(com/toly1994/dp/creational/singleton/world/enum_/World, name);
    }

    private World(String s, int i){
        super(s, i);
        initWorld();
        System.out.println("\u4E16\u754C\u5DF2\u521B\u5EFA");
    }

    private void initWorld(){//私有化構造
    }

    public static final World INSTANCE;//靜態實例
    private static final World $VALUES[];

    static //靜態代碼塊初始實例
    {
        INSTANCE = new World("INSTANCE", 0);
        $VALUES = (new World[] {
            INSTANCE
        });
    }
}
複製代碼

3、單例下的反序列化與反射

單例的價值在於一個程序中只用一個該對象實例
若是有惡意份子經過反射建立了另外一個世界會怎麼樣?

1.單例的測試

經過debug看出兩次獲取的都是同一個世界,這就是單一實例

單例測試.png

public class God {
    public static void main(String[] args) {
        World world1 = World.getInstance();
        World world2 = World.getInstance();
    }
}
複製代碼

2.經過反射建立實例

可見world3的內存地址已經不同了,說明出現了第二個世界,也就是單例的失效

反射測試.png

public class God {
    public static void main(String[] args) {
        World world1 = World.getInstance();
        World world2 = World.getInstance();
        //經過反射建立
        Class<World> worldClass = World.class;
        try {
            Constructor<World> constructor = worldClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            World world3 = constructor.newInstance();
            System.out.println(world3==world2);//false
            System.out.println(world1==world2);//true
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

3.經過反序列化建立對象

若是你的單例類有序列化的需求(如,單例對象本地存儲,單例對象網絡傳輸) 反序列化造成的實例也並不是原來的實例

反序列化.png

---->[World]-------------
public class World implements Serializable {

---->[God]-------------
public class God {
    public static void main(String[] args) {
        World world1 = World.getInstance();
        World world2 = World.getInstance();

        //經過反射建立
        Class<World> worldClass = World.class;
        try {
            Constructor<World> constructor = worldClass.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            World world3 = constructor.newInstance();
            System.out.println(world3 == world2);//false
            System.out.println(world1 == world2);//true
        } catch (Exception e) {
            e.printStackTrace();
        }
        //經過反序列化建立對象
        try {
            //序列化輸出
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("world.obj"));
            oos.writeObject(world1);
            //反序列化建立對象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("world.obj"));
            World world4 = (World) ois.readObject();
            ois.close();
            System.out.println(world1 == world4);//false
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

4.發序列化的解決方案

經過反序列化時的鉤子函數:readResolve來控制序列化對象實例

反序列化的防治.png

---->[World]-------------
//解決反序列化建立實例的問題,readResolve建立的對象會直接替換io流讀取的對象
private Object readResolve() throws ObjectStreamException {
    return getInstance();
}
複製代碼

4、結尾小述

1.單例抉擇
[1] 肯定以及確定不會在單線程中用到的單例對象,能夠用單線程的懶漢
[2] 單例對象不大,並不介意在類加載時實例化對象,枚舉首選,其次是餓漢
[3] 若是要在多線程的時候徹底防反射,雙檢鎖模式不能夠。可以使用靜態初始化的幾種模式,在建立對象時進行非空校驗便可
複製代碼
2.常見的單例
java.util.Calendar 標準單例,經過Calendar.getInstance方法獲取對象
java.lang.System 徹底單例,不提供外部構造方法,所有以靜態方法提供服務
android.view.LayoutInflater 標準單例 ,經過LayoutInflater.from(Context)方法獲取對象
複製代碼

後記:捷文規範

1----本文由張風捷特烈原創,轉載請註明
2----若是有什麼想要交流的,歡迎留言。也能夠加微信:zdl1994328
3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正
4----看到這裏,我在此感謝你的喜歡與支持,掃碼關注-編程之王

icon_wx_200.png
相關文章
相關標籤/搜索