1. 程式人生 > >原始碼學習之設計模式(單例模式)

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

眾所周知,單例模式分為餓漢式和懶漢式,昨天在看了《spring5核心原理與30個類手寫實戰》之後才知道餓漢式有很多種寫法,分別適用於不同場景,避免反射,執行緒不安全問題。下面就各種場景、採用的方式及其優缺點介紹。

餓漢式 (絕對的執行緒安全)

程式碼示例

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

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;
    }

}

這裡需要解釋下,童鞋們都知道一個物件使用要經歷一下步驟:

  • 為物件分配記憶體

  • 初始化物件

  • 例項物件指向第一布分配的記憶體地址

    在java中JVM為了提高執行效率,會進行指令重排。那什麼時指令重排呢?指令重排是指JVM為了優化指令,提高程式的執行效率,在不影響單執行緒執行結果的情況下,進行指令重排序,以期提高並行度。

有上述可以指令重排在單執行緒情況下,對程式的執行不會產生影響,但在多執行緒情況下就不一定了。所以上述過程的執行順序可能發生變化,進而導致程式並不會按照預想的執行。

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

1.如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

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 檢視ConstructornewInstsnce() 方法可知,在方法體做了判斷,如果是列舉型別則直接丟擲異常。

看到這個詞,有的小夥伴的心裡就想什麼是容器式單例。容器式單例就是在單例類中維護一個類似與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個類手寫實戰》以及網際網路上的內容。如要轉載請註明來源。
如有錯誤,請評論或者私聊我,歡迎探討技術問