java設計模式—單例模式(包含單例的破壞)

什麼是單例模式?

保證一個了類僅有一個實例,並提供一個訪問它的全局訪問點。java

單例模式的應用場景?

  1. 網站的計數器,通常也是採用單例模式實現,不然難以同步;
  2. Web應用的配置對象的讀取,通常也應用單例模式,這個是因爲配置文件是共享的資源;
  3. 數據庫鏈接池的設計通常也是採用單例模式,由於數據庫鏈接是一種數據庫資源;
  4. 多線程的線程池的設計通常也是採用單例模式,這是因爲線程池要方便對池中的線程進行控制。

單例的優缺點?

優勢:數據庫

  • 提供了對惟一實例的受控訪問;
  • 因爲在系統內存中只存在一個對象,所以能夠 節約系統資源,當 須要頻繁建立和銷燬的對象時單例模式無疑能夠提升系統的性能;
  • 避免對共享資源的多重佔用

缺點:安全

  • 不適用於變化的對象,若是同一類型的對象老是要在不一樣的用例場景發生變化,單例就會引發數據的錯誤,不能保存彼此的狀態;
  • 因爲單利模式中沒有抽象層,所以單例類的擴展有很大的困難;
  • 單例類的職責太重,在必定程度上違背了「單一職責原則」。

單例的建立方式

1. 餓漢式

類初始化時,會當即加載該對象,線程安全,效率高。多線程

/**
 * @Author 劉翊揚
 * @Version 1.0
 */
public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry() {}

    public static SingletonHungry getInstance() {
        return instance;
    }
}

驗證:ide

public class Main {
    public static void main(String[] args) {
        SingletonHungry instance1 = SingletonHungry.getInstance();
        SingletonHungry instance2 = SingletonHungry.getInstance();
        System.out.println(instance1 == instance2);      // 結果是true
    }
}

優勢:僅實例化一次,線程是安全的。獲取實例的速度快
缺點:類加載的時候當即實例化對象,可能實例化的對象不會被使用,形成內存的浪費。函數

2. 使用靜態代碼塊

/**
* @author 劉翊揚
*/
public class HungrySingleton2 {
   
   private static HungrySingleton2 instance = null;
   
   private HungrySingleton2() {}

   static {
       instance = new HungrySingleton2();
   }
   
   private HungrySingleton2() {}
   
   public static HungrySingleton2 getInstance() {
       return instance;
   }
   
}

3. 懶漢式

public class SingletonLazy {

    private static SingletonLazy instance;

    private SingletonLazy() {}

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

優勢:在使用的時候,建立對象,節省系統資源
缺點:性能

  1. 若是獲取實例時,初始化的工做量較多,加載速度會變慢,影響系統系能
  2. 每次獲取對象,都要進行非空檢查,系統開銷大
  3. 非線程安全,當有多個線程同時調用 getInstance()方法,時,會有線程安全問題,可能致使建立多個對象

4. 靜態內部類

/**
* @Author 劉翊揚
* @Version 1.0
*/
public class SingletonDemo03 {

   private SingletonDemo03() {}

   public static class SingletonClassInstance {
       private static final SingletonDemo03 instance = new SingletonDemo03();
   }

   public static SingletonDemo03 getInstance() {
       return SingletonClassInstance.instance;
   }
}

優點:兼顧了懶漢模式的內存優化(使用時才初始化)以及餓漢模式的安全性(不會被反射入侵)。優化

劣勢:須要兩個類去作到這一點,雖然不會建立靜態內部類的對象,可是其 Class 對象仍是會被建立,並且是屬於永久帶的對象。網站

5. 使用枚舉

枚舉自己就是單例的,通常在項目中定義常量。
例如:this

/**
 * @Author 劉翊揚
 * @Version 1.0
 */
public enum  ResultCode {
    
    SUCCESS(200, "SUCCESS"),
    ERROR(500, "ERROR");
    
    private Integer code;
    
    private String msg;

    ResultCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}
/**
 * @Author 劉翊揚
 * @Version 1.0
 */
public class User {

    private User() {}

    public static User getInstance() {
        return SingletonDemo04.INSTANCE.getInstance();
    }

    private static enum SingletonDemo04 {
        INSTANCE;
        // 枚舉元素爲單例
        private User user;

        SingletonDemo04() {
            user = new User();
        }

        public User getInstance() {
            return user;
        }
    }
}

解決線程安全問題

使用雙重檢測鎖

public class LazySingletonDemo1 {

    private static LazySingletonDemo1 instance = null;
    public static LazySingletonDemo1 getInstance() {
        if (instance == null) {
            synchronized (LazySingletonDemo1.class) {
                if (instance == null) {
                    instance = new LazySingletonDemo1();
                }
            }
        }
        return instance;
    }
}

這裏使用雙重檢測,是爲了防止,當實例存在的時候,不在走同步鎖,減小使用鎖帶來的性能的消耗。

單例模式必定能保證只有一個實例對象嗎?

答案是:不能

破壞單例的兩種方式:

  1. 反射
  2. 反序列化

1. 反射破壞

經過反射是能夠破壞單例的,例如使用內部類實現的單例。經過反射獲取其默認的構造函數,而後使默認構造函數可訪問,就能夠建立新的對象了。

/**
 * @Author 劉翊揚
 * @Version 1.0
 */
public class ReflectionDestroySingleton {

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        SingletonLazy instance = SingletonLazy.getInstance();
        Class aClass = SingletonLazy.class;
        // 獲取默認的構造方法
        Constructor<SingletonLazy> declaredConstructor = aClass.getDeclaredConstructor();
        // 使默認構造方法可訪問
        declaredConstructor.setAccessible(true);
        // 建立對象
        SingletonLazy instance2 = declaredConstructor.newInstance();
        System.out.println(instance == instance2);  // 結果是:false
    }
}

怎麼阻止???
能夠增長一個標誌位,用來判斷構造函數是否被調用了。

public class SingletonLazy {

    // 標誌位
    private static Boolean isNew = false;

    private static SingletonLazy instance;

    private SingletonLazy() {
        synchronized (SingletonLazy.class) {
            if (!isNew) {
                isNew = true;
            } else {
                throw new RuntimeException("單例模式被破壞!");
            }
        }
    }

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

再次運行:

注意:
增長標誌位的確能阻止單例的破壞,可是這個代碼有一個BUG,那就是若是單例是先用的反射建立的,那若是你再用正常的方法getInstance()獲取單例,就會報錯。由於此時標誌位已經標誌構造函數被調用過了。這種寫法除非你能保證getInstance先於反射執行。

public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class aClass = SingletonLazy.class;
        // 獲取默認的構造方法
        Constructor<SingletonLazy> declaredConstructor = aClass.getDeclaredConstructor();
        // 使默認構造方法可訪問
        declaredConstructor.setAccessible(true);
        // 建立對象
        SingletonLazy instance2 = declaredConstructor.newInstance();
        System.out.println("反射實例:" + instance2);
        // 再次調用
        SingletonLazy instance = SingletonLazy.getInstance();
        System.out.println(instance == instance2);  // 結果是:false
    }

結果:

反序列化

SingletonLazy要實現Serializable接口

public static void main(String[] args) throws Exception {
        //序列化
        SingletonLazy instance1 = SingletonLazy.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile.txt"));
        out.writeObject(SingletonLazy.getInstance());

        File file = new File("tempfile.txt");
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        //調用readObject()反序列化
        SingletonLazy instance2 = (SingletonLazy) in.readObject();
        System.out.println(instance1 == instance2); // 結果是:false
    }

原理解釋:
反序列化爲何能生成新的實例,必須從源碼看起。這裏分析readObject()裏面的調用源碼。會發現readObject()方法後進入了readObject0(false)方法。

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);  //經過debug會發現進入此方法
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

分析readObject0方法,經過debug進入了readOrdinaryObject()方法。

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                .... 省略部分源碼
    
                case TC_OBJECT:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared)); // 經過debug發現進入到了readOrdinaryObject()方法。

               .... 省略部分源碼
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

經過分析,readOrdinaryObject()中有兩處關鍵代碼,其中關鍵代碼1中的關鍵語句爲:

此處代碼是經過描述對象desc,先判斷類是否能夠實例化,若是能夠實例化,則執行desc.newInstance()經過反射實例化類,不然返回null。

obj = desc.isInstantiable() ? desc.newInstance() : null;

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
          // 關鍵代碼=========
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

經過斷點調試,發現其調用了desc.newInstance()方法。

咱們知道調用newInstance()方法,必定會走類的無參構造方法,可是上面經過debug咱們發現,cons的類型是Object類型,因此,這裏面應該是調用類Object的無參構造方法,而不是SingletonLazy類的無參構造

那麼怎麼改造呢????

繼續debug調試:查看readOrdinaryObject()方法

發現,desc.hasReadResolveMethod()這個方法返回的false,因此致使沒有執行if條件下面的語句。

desc.hasReadResolveMethod() // 從方法名能夠看到,這個方法的名字是檢查desc這個(SingleLazy)對象有沒有readResolve()方法。

咱們如今阻止破壞單例,應該只須要在SingleLazy類中,實現本身的readResolve()方法便可。

public Object readResolve() {
        return instance;
    }

如今咱們在看看運行的結果:爲true

大功告成。。。。

相關文章
相關標籤/搜索