java單例模式看這一篇就夠了

什麼是單例模式

單例模式(Singleton Pattern)是指確保一個類在任何情況下都絕對只有一個實例,並提供一個全局訪問點。單例模式屬於創建型模式

單例模式的常見寫法

一、餓漢式單例

顧名思義餓漢式單例是在類加載的時候就立即初始化,並且創建單例對象。絕對線程安全,在線程還沒出現以前就被實例化了,不可能存在訪問安全問題

優點

沒有加任何的鎖、執行效率比較高,在用戶體驗上來說,比懶漢式更好

缺點

類加載的時候就初始化,不管用與不用都佔着空間,如果項目中有大量單例對象,則可能會浪費大量內存空間

示例

package com.zwx.design.pattern.singleton.hungry;

public class HungrySingleton {
    private static final HungrySingleton hungrySigleton = new HungrySingleton();

    private HungrySingleton() {
    }
    
    public static HungrySingleton getInstance(){
        return hungrySigleton;
    }
}

或者也可以利用靜態代碼塊的方式實現餓漢式單例

package com.zwx.design.pattern.singleton.hungry;

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySigleton;
    static {
        hungrySigleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance(){
        return hungrySigleton;
    }
}

這兩種寫法都非常的簡單,也非常好理解,餓漢式適用在單例對象較少的情況

二、懶漢式單例

懶漢式單例的特點是:被外部類調用的時候內部類纔會加載

示例1(普通寫法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance(){
        if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

上面的寫法是最簡單的一種懶漢式單例寫法,但是存在線程安全問題,多線程情況下會有一定機率返回多個單例對象,這明顯違背了單例對象原則,那麼如何優化上面的代碼呢?答案就是加上synchronized關鍵字

示例2(synchronized寫法)

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.hungry.HungrySingleton;

public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance(){
        if(null == lazySingleton){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

示例2的寫法僅僅是在getInstance()方法上面加了synchronized關鍵字,其他地方沒有任何變化。用 synchronized 加鎖,在線程數量比較多情況下,如果CPU分配壓力上升,會導致大批量線程出現阻塞,從而導致程序運行性能大幅下降。那麼,有沒有一種更好的方式,既 兼顧線程安全又提升程序性能呢?答案是肯定的。接下來就在介紹一種雙重檢查鎖(double-checked locking)單例寫法

示例3(DCL寫法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton() {
    }

    public static LazyDoubleCheckSingleton getInstance(){
        if(null == lazySingleton){//1
            synchronized (LazyDoubleCheckSingleton.class){//2
                if(null == lazySingleton){//3
                    lazySingleton = new LazyDoubleCheckSingleton();//4
                }
            }
        }
        return lazySingleton;//5
    }
}

這裏的寫法將同步放在了方法裏面的第一個非空判斷之後,這樣可以確保對象不爲空的時候不會被阻塞,但是第二個非空判斷的意義是什麼呢?我們假設線程A首先獲得鎖,進入了第3行,還沒有釋放鎖的時候,線程B又進來了,這時候因爲線程還沒有執行對象初始化,所以判空成立,會進入第2行等待獲得鎖,這時候當線程A釋放鎖之後,線程B會進入到第3行,這時候因爲第二個判空判斷對象不爲空了,所以就會直接返回,如果沒有第2個判空,這時候就會產生新的對象了,所以需要兩次判空!

大家可能注意到這裏的變量定義上加了volatile關鍵字,爲什麼呢?這是因爲DCL在可能會存在失效的情況:
第4行代碼:lazySingleton = new LazyDoubleCheckSingleton();
大致存在以下三步:
(1)、分配內存給對象
(2)、初始化對象
(3)、將初始化好的對象和內存地址建立關聯(賦值)
而這3步由於CPU指令重排序,不能保證一定按順序執行,假如線程A正在執行new的操作,第1步和第3步都執行完了,但是第2步還沒執行完,這時候線程B進入到方法中的第1行代碼,判空不成立,所以直接返回了對象,而這時候對象並沒有初始化完全,所以就會報錯了,解決這個問題的辦法就是使用volatile關鍵字,禁止指令重排序(jdk1.5之後),保證按順序執行上面的三個步驟。想要詳細瞭解volatile關鍵字是如何解決重排序問題的,可以點擊這裏

示例4(內部類寫法)

package com.zwx.design.pattern.singleton.lazy;

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){
    }
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

上面的寫法巧妙的利用了內部類的特性,LazyHolder裏面的邏輯需要等到外面方法調用時才執行。
這種寫法看起來很完美,沒有加鎖,也保證了懶加載,但是這種單例模式也有問題,那就是可以被反射或者序列化破壞單例,下面我們寫一個反射破壞單例的例子

package com.zwx.design.pattern.singleton.lazy;

import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) throws Exception {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object o1 = constructor.newInstance();
        Object o2 = LazyInnerClassSingleton.getInstance();
        System.out.println(o1 == o2);//false
    }
}

上面這個結果輸出的結果爲false,說明產生了2個對象,當然,要防止反射破壞單例很簡單,我們可以把上面例子中的構造方法加一個判斷就可以了:

 private LazyInnerClassSingleton(){
        //防止反射攻擊
       if(null != LazyHolder.LAZY){
           throw new RuntimeException("不允許構造多個實例");
       }
    }

這樣雖然防止了反射破壞單例,但是依然可以被序列化破壞單例,下面就讓我們驗證一下序列化是如何破壞單例的!
首先對上面的類實現序列化接口

public class LazyInnerClassSingleton implements Serializable

接下來開始對單例對象類進行序列化和反序列化測試:

package com.zwx.design.pattern.singleton.lazy;

import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;

import java.io.*;
import java.lang.reflect.Constructor;

public class LazyInnerClassSingletonTest {

    public static void main(String[] args) throws Exception {
        LazyInnerClassSingleton s1 = null;
        LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();

        FileOutputStream fos = null;

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

            FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (LazyInnerClassSingleton)ois.readObject();
            ois.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);//false
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

這時候輸出結果爲false,說明產生了2個對象,那麼我們應該如何防止序列化破壞單例呢?我們可以對LazyInnerClassSingleton類加上readResolve方法就可以防止序列化破壞單例

package com.zwx.design.pattern.singleton.lazy;

import java.io.Serializable;

public class LazyInnerClassSingleton implements Serializable {

    private LazyInnerClassSingleton(){
        //防止反射攻擊
       if(null != LazyHolder.LAZY){
           throw new RuntimeException("不允許構造多個實例");
       }
    }
    //防止序列化破壞單例
    private Object readResolve(){
        return LazyHolder.LAZY;
    }
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.LAZY;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

這是因爲JDK源碼中會檢驗一個類中是否存在一個readResolve()方法,如果存在,則會放棄通過序列化產生的對象,而返回原本的對象,也就是說,在校驗是否存在readResolve()方法前產生了一個對象,只不過這個對象會在發現類中存在readResolve()方法後丟掉,然後返回原本的單例對象,保證了單例的唯一性,這種寫法雖然保證了單例唯一,但是過程中類也是會被實例化兩次,假如創建對象的頻率增大,就意味着內存分配的開銷也隨之增大,那麼有沒有辦法從根本上解決問題呢?那麼下面就讓繼續介紹一下注冊式單例

三、註冊式單例

註冊式單例就是將每一個實例都保存到某一個地方,然後使用唯一的標識獲取實例

示例1(容器式)

package com.zwx.design.pattern.singleton.register;

public class ContainerSingleton {
    private ContainerSingleton(){
    }

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return obj;
            }
            return ioc.get(className);
        }
    }
}

容器式寫法適用於創建實例非常多的情況,便於管理。但是,是非線程安全的,spring中的單例就是屬於此種寫法

示例2(枚舉式)

package com.zwx.design.pattern.singleton.register;

public enum EnumSingleton {
    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
   
}

枚舉式單例是《Effective java》一書中推薦的寫法,這種寫法避免了上面的內部類寫法中存在的問題(雖然結果唯一,但是過程產生了多個實例對象),是一種效率較高的寫法

四、ThreadLocal式單例

ThreadLocal不能保證其創建的對象是全局唯一,但是能保證在單個線程中是唯一的,天生的線程安全

示例

package com.zwx.design.pattern.singleton.threadlocal;

public class ThreadLocalSingleton {
    private ThreadLocalSingleton() {

    }
    private static final ThreadLocal<ThreadLocalSingleton> singleton =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    public static ThreadLocalSingleton getInstance(){
        return singleton.get();
    }
}

測試

package com.zwx.design.pattern.singleton.threadlocal;

import com.zwx.design.pattern.singleton.ExectorThread;
import com.zwx.design.pattern.singleton.ExectorThread3;

public class ThreadLocalSingletonTest {

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

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ":" + singleton);
            }
        });
        t1.start();
    }
}

在這裏插入圖片描述
反覆測試可以發現同一個線程獲得的對象是唯一的,不同對象則不唯一

總結

單例模式可以保證內存裏只有一個實例,減少了內存開銷;可以避免對資源的多重佔用,單例模式的寫法很多,大家可以根據自己的業務需求選擇合適自己的單例方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章