源碼學習之設計模式(單例模式)

衆所周知,單例模式分爲餓漢式和懶漢式,昨天在看了《spring5核心原理與30個類手寫實戰》以後才知道餓漢式有不少種寫法,分別適用於不一樣場景,避免反射,線程不安全問題。下面就各類場景、採用的方式及其優缺點介紹。java

餓漢式 (絕對的線程安全)

代碼示例

1.第一種寫法 ( 定義即初始化)spring

public class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}複製代碼

  1. 第二種寫法 (靜態代碼塊)
public class Singleton{
    private static final Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}複製代碼

餓漢式基本上就這兩種寫法。在spring框架中IoC的ApplicantsContext 就是使用的餓漢式單例,保證了全局只有一個ApplicationContext ,在應用啓動後就能獲取實例,以便於進行接下來的操做.編程

優勢

因其在程序啓動後就已經初始化,也不須要任何鎖保證線程安全 ,因此執行效率高複製代碼
缺點

由於在程序啓動後就已經進行了初始化,即使是不用也進行了初始化,因此不管什麼時候都佔用內存空間,浪費了內存空間。

複製代碼

懶漢式 (線程安全須要另外的操做)

代碼示例

  1. 第一種寫法
public class Singleton{
    private static final Singleton instance = null;
 
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }

}複製代碼

上面的代碼不難看出,在單線程下執行是沒有問題的,但在多線程狀況下,線程執行速度和順序沒法控制肯定,故有可能會產生多個實例對象,這樣就違背了單例模式的初衷了。安全

  1. 第二種寫法

加鎖保證線程安全(synchronized 關鍵字)多線程

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

}複製代碼

​ 能夠看到在getInstance()上加了synchronized 關鍵字,就能保證線程同步。但又有一個問題:使用synchronized關鍵字是,當一個線程調用獲取實例的方法時,會鎖住整個類,其餘的線程再調用,會使線程狀態由 RUNNING 變成 MONITOR ,進而致使線程阻塞,執行效率降低;知道這個線程執行完實例方法,其餘線程才能繼續執行,兩個線程時,效率降低還在能夠接受範圍內,但在實際應用場景中,使用線程池來管理線程的調度,會有大量的線程,若是這些線程都阻塞了,其結果能夠預見。併發

上述問題有什麼更好的問題解決呢?使用雙重檢查鎖機制能夠完美的解決這個問題。其代碼以下複製代碼
public class Singleton{
    private volatile  static final Singleton instance = null;
 
    private Singleton() {}
    public  static Singleton getInstance() {
        if(instance == null){
            synchronized(Singleton.class){
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            
        }
        return instance;
    }

}複製代碼

這裏須要解釋下,童鞋們都知道一個對象使用要經歷一下步驟:app

  • 爲對象分配內存
  • 初始化對象
  • 實例對象指向第一布分配的內存地址
在java中JVM爲了提升執行效率,會進行指令重排。那什麼時指令重排呢?**指令重排**是指JVM爲了優化指令,提升程序的運行效率,在不影響單線程執行結果的狀況下,進行指令重排序,以期提升並行度。複製代碼

有上述能夠指令重排在單線程狀況下,對程序的執行不會產生影響,但在多線程狀況下就不必定了。因此上述過程的執行順序可能發生變化,進而致使程序並不會按照預想的執行。框架

爲解決上述問題以及保證併發編程的正確性,java中定義了 **happens-before**原則。在 《JSR-133:Java Memory Model and Thread Specification》 書中關於happens-before定義是這樣的:複製代碼

1.若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。ide

2.兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法。性能

在Java中 爲 避免指令重排出現,引入了volatile 關鍵字。正如你所看到那樣在實例對象前就能保證執行結果的正確性。當一個線程調用` getInstance()` 方法時,執行到synchronized關鍵字時就會上鎖,其餘線程也調用時就會發生阻塞,固然這種阻塞不是鎖住整個類,而是僅僅鎖住了方法。如過方法中的邏輯不是太複雜的話,對於外界來講是感知不到的。複製代碼

​ 這種方法終歸仍是要加鎖的,只要加鎖就會對程序性能產生影響。有什麼解決辦法能夠實現不加鎖,又能保證線程安全呢?

內部類:是指 一個類定義在另外一個類裏面或者一個方法裏面 的類。有如下特色:

  • 隱藏機制:內部封裝性好,即使是同一個包下的類也不能直接訪問
  • 內部類能夠訪問外圍類的私有數據
  • 內部類對象能夠不依賴外部實例被實例化

靜態內部類:顧名思義 就是在內部類上加個static關鍵字 ,其特色有:

  • 能夠訪問外部類靜態成員
  • 能夠定義靜態成員,非靜態內部類不能夠

靜態內部類在載入Java的時候默認不加載,只有調用時進行加載。根據此特色雙鎖檢查機制的單例模式能夠改進使用靜態內部類。

  1. 使用靜態內部類

代碼示例

public class Singleton{
    private Singleton() {}
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    //static是爲了單例內存共享,保證這個方法不會被重寫,重載
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }

}複製代碼

上述方法及解決了餓漢式的內存浪費問題,又解決了懶漢式的鎖的性能問題。

進一步思考

反射破壞單例

你們都知道在Java的各個框架中由於要實現某種功能,不可避免的使用到反射。反射有破壞封裝性和性能低下的問題。在這裏不考慮性能,只考慮封裝性被破壞的問題。調用者使用反射,破壞了封裝性,進而使實例有可能不止一個,這樣就違背了使用單例模式的初衷。

如何解決呢?很簡單,就是在建立另外的對象拋出異常,警告調用者,使其按照咱們預想的方式進行調用。

代碼示例

public class Singleton{
    private Singleton() {
        if(SingletonIner.instance!=null){
            throw new RuntimeException("不容許建立多個實例");
        }
    }
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }

}複製代碼

上面代碼可使調用者按照咱們的想法使用。

序列化破壞單例

在實際應用中,爲保存對象到磁盤或其餘的存儲介質,不可避免的要使用序列化。一個單例建立好以後,將其序列化保存在磁盤上,下次使用時在反序列化取出放到內存中使用。反序列化後的對象會從新分配內存,即從新建立,這樣就違反了單例模式的初衷。以使用靜態內部類的代碼爲咱們單例模式類,下面進行簡單測試。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main{
    public static void main(String[] args) {
        Singleton s1=null;
        Singleton s2 = Singleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos=new FileOutputStream("singleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis =new FileInputStream("singleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (Singleton)ois.readObject();
            ois.close();
            System.out.println(s1==s2)

        }
        catch(Exception e){
                e.printStackTrace();
        }
    }
}複製代碼

上面代碼運行後發現,輸出居然時false,這就說明反序列化後和序列話前的對象不是同一個,實例化了兩次,根本不符合單例模式的原則。

如何改進呢? 改進 的方法也很簡單就是增長readResolve() 方法就能夠。下面看代碼

import java.io.Serializable;

public class Singleton implements Serializable{
    private Singleton() {
        if(SingletonIner.instance!=null){
            throw new RuntimeException("不容許建立多個實例");
        }
    }
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }
    private Object readResolve() {
        return SingletonIner.instance;
    }

}複製代碼

深究一下,爲何會這樣呢?下面咱們來看看ObjectInputStream 裏的readObject() 方法一探究竟。代碼以下:

/**
     * Read an object from the ObjectInputStream.  The class of the object, the
     * signature of the class, and the values of the non-transient and
     * non-static fields of the class and all of its supertypes are read.
     * Default deserializing for a class can be overridden using the writeObject
     * and readObject methods.  Objects referenced by this object are read
     * transitively so that a complete equivalent graph of objects is
     * reconstructed by readObject.
     *
     * <p>The root object is completely restored when all of its fields and the
     * objects it references are completely restored.  At this point the object
     * validation callbacks are executed in order based on their registered
     * priorities. The callbacks are registered by objects (in the readObject
     * special methods) as they are individually restored.
     *
     * <p>Exceptions are thrown for problems with the InputStream and for
     * classes that should not be deserialized.  All exceptions are fatal to
     * the InputStream and leave it in an indeterminate state; it is up to the
     * caller to ignore or recover the stream state.
     *
     * @throws  ClassNotFoundException Class of a serialized object cannot be
     *          found.
     * @throws  InvalidClassException Something is wrong with a class used by
     *          serialization.
     * @throws  StreamCorruptedException Control information in the
     *          stream is inconsistent.
     * @throws  OptionalDataException Primitive data was found in the
     *          stream instead of objects.
     * @throws  IOException Any of the usual Input/Output related exceptions.
     */
    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);
            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();
            }
        }
    }複製代碼

根據註釋,咱們知道readObject() 方法讀取一個對象的類,類的簽名以及該類機器全部超類的非瞬時和非靜態的值。咱們看到在try後面又調用了重寫的readObject0() 方法,其代碼以下:

/**
     * Underlying readObject implementation.
     */
    private Object readObject0(boolean unshared) throws IOException {
               .......

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
             .......
           
    }複製代碼

因篇幅的問題我省略了不重要的代碼。

由上面看到,在TC_OBJECT處又調用了`readOrdinaryObject()` 方法,其源碼以下:

/**
     * Reads and returns "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) object, or null if object's
     * class is unresolvable (in which case a ClassNotFoundException will be
     * associated with object's handle).  Sets passHandle to object's assigned
     * handle.
     */
    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;
    }複製代碼

由上述代碼可知,由調用了ObjectStreamClassisInstanctiable() 方法,方法體很是簡單,源碼以下 :

/**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }複製代碼

其做用就是構造方法是否爲空,構造方法不爲空就返回true。這意味着只要時無參構造方法就會實例化。

再回去看 readOrdinaryObject() 的源碼。先是判斷readResloveMethod 是否爲空,經過全局查找可知在私有方法ObjectStreamClass() 給其賦值,賦值代碼以下:

readResolveMethod  = gerInheritableMethod(c1,"readResolve",null,Object.class);複製代碼

以後上述的邏輯找到一個 readResolve() 方法若是存在就調用 invokeReadResolve() 方法,其代碼以下:

/**
     * Invokes the readResolve method of the represented serializable class and
     * returns the result.  Throws UnsupportedOperationException if this class
     * descriptor is not associated with a class, or if the class is
     * non-serializable or does not define readResolve.
     */
    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }複製代碼

invokeReadResource() 方法又使用反射調用 readResolveMethod() ,進而執行readResolve() 方法。

經過分析源碼能夠看出,readResolve() 方法雖然解決了單例模式被破壞的問題,可是其實例化兩次,只不過新建立的對象被覆蓋了而已 。若是建立的對象動做發生加快,就意味着內存開銷也隨之增大。這個問題如何解決呢?使用註冊式單例便可完美解決上訴問題。

註冊式單例

  1. 枚舉式單例
代碼示例

public enum EnumSingleton{
    INSTANCE;
    private Object data;
    

    /**
     * @return Object return the data
     */
    public Object getData() {
        return data;
    }

    /**
     * @param data the data to set
     */
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}複製代碼

通過反編譯分析源碼可知枚舉式單例是在靜態代碼塊中爲INSTANCE賦值,使餓漢式單例的體現。

那麼序列化和反序列化可否破壞嗎枚舉式單例呢? 答案是不能。同查看源碼可知枚舉類型是經過類名和對象名找到全局惟一的對象。因此,枚舉對象不可能加載屢次。

那麼反射呢?答案也是不能。在程序運行時會報java.lang.NoSuchMethodException 異常,其意思爲沒有找到無參的構造方法。查看java.lang.Enum 源碼可知枚舉類型只有一個protect 構造方法。通過測試,使用反射直接實例化枚舉對象時會出現 Cannot reflectively create objects 查看Constructor newInstsnce() 方法可知,在方法體作了判斷,若是是枚舉類型則直接拋出異常。

看到這個詞,有的小夥伴的內心就想什麼是容器式單例。容器式單例就是在單例類中維護一個相似與Map的容器,這種方式在Spring中是很是常見的,衆所周知,Spring的Bean是全局單例的;Spring在內部維護着一個Map結構。在 org.springframework.beans.factory.support 包下 SimpleBeanDefinitionRegistry 爲咱們完美的解釋容器式單例,其源碼以下:

public class SimpleBeanDefinitionRegistry extends SimpleAliasRegistry implements BeanDefinitionRegistry {

    /** Map of bean definition objects, keyed by bean name. */
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);


    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {

        Assert.hasText(beanName, "'beanName' must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        this.beanDefinitionMap.put(beanName, beanDefinition);
    }

    @Override
    public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
        if (this.beanDefinitionMap.remove(beanName) == null) {
            throw new NoSuchBeanDefinitionException(beanName);
        }
    }

    @Override
    public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
        BeanDefinition bd = this.beanDefinitionMap.get(beanName);
        if (bd == null) {
            throw new NoSuchBeanDefinitionException(beanName);
        }
        return bd;
    }

    @Override
    public boolean containsBeanDefinition(String beanName) {
        return this.beanDefinitionMap.containsKey(beanName);
    }

    @Override
    public String[] getBeanDefinitionNames() {
        return StringUtils.toStringArray(this.beanDefinitionMap.keySet());
    }

    @Override
    public int getBeanDefinitionCount() {
        return this.beanDefinitionMap.size();
    }

    @Override
    public boolean isBeanNameInUse(String beanName) {
        return isAlias(beanName) || containsBeanDefinition(beanName);
    }

}複製代碼

其中BeanDefinition 是一個接口,儲存着各個單例對象的信息,由其實現類實現。對象名做爲Map的Key,BeanDefinition 做爲Map的值,維護着這個map 保證每一個對象全局單例.

由於Spring比較複雜,討論暫告一段落。下面會到咱們的主題,那咱們的 singleton 類如何實現容器式單例呢。下面看代碼:

import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;

public class Singleton {
    private static Map <String,Object > ioc =new ConcurrentHashMap();
    private Singleton() {}
    public  static Object getInstance(String name) {
     synchronized(ioc) {
         if (!ioc.containsKey(name)){
             Object o=null;
             try {
                 o=Class.forName(name).newInstance();
                 ioc.put(name, o);
                
             }catch(Exception e) {
                 e.printStackTrace();
             }
             return o;
         }
         else {
             return ioc.get(name);
         }
     }
    }
    

}複製代碼

容器式單例適用於單例實例對象比較多的狀況下,方便管理。值得注意的是,他是線程不安全的。

註冊式單例就包括上面兩種形式,每一個都有不一樣的應用場景以及特色,要根據實際狀況靈活選擇。

下面我來介紹一種特殊的單例模式-----擁有 ThreadLocal 單例模式。

擴展

ThreadLocal 與單例模式

話很少說,直接看代碼。

public class Singleton {
    private static final ThreadLocal<Singleton> instance = new ThreadLocal<> (){
        @Override
        protected Singleton initialValue() {
            return new Singleton();
        }
    };
    private Singleton() {}
    public  static Object getInstance() {
        return instance.get();
    }
    

}複製代碼

爲何說他特殊呢?由於加了 ThreadLocal 關鍵字的單例類是線程內單例的,單線程共享不是單例的。你們能夠測試下,使用下面的測試代碼。

public class Main{
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());


        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        } ;

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        System.out.println("end");
    }
}複製代碼

執行結果以下:

image-20191216180630142

測試後發現主線程不管執行多少次,獲取的實例都是同一個,而兩個子線程卻得到了不一樣的實例。

聲明

本文章爲做者原創,其中參考了《spring5核心原理與30個類手寫實戰》以及互聯網上的內容。如要轉載請註明來源。若有錯誤,請評論或者私聊我,歡迎探討技術問題

相關文章
相關標籤/搜索