設計模式-單例模式☞10種不同的豔遇 讓你精通單例

1.引言

單例設計模式(Singleton Pattern)是最簡單且常見的設計模式之一,在它的核心結構中只包含一個被稱爲單例的特殊類。經過單例模式能夠保證系統中一個類只有一個實例並且該實例易於外界訪問,從而方便對實例個數的控制並節約系統資源。若是但願在系統中某個類的對象只能存在一個,避免多實例對象的狀況下引發邏輯性錯誤(實例化數量可控)單例模式是最好的解決方案。java

  • 一、單例類只能有一個實例。
  • 二、單例類必須本身建立本身的惟一實例。
  • 三、單例類必須給全部其餘對象提供這一實例。

2.概述

Java中,單例模式主要分四種:懶漢式單例、餓漢式單例、登記式單例、ThreadLocal單例模式四種spring

  • 懶漢:非線程安全,須要用必定的風騷操做控制,裝逼失敗有可能致使看一週的海綿寶寶
  • 餓漢:天生線程安全,ClassLoad的時候就已經實例化好,該操做過於風騷會形成資源浪費
  • 單例註冊表:Spring初始化Bean的時候,默認單例用的就是該方式
  • 單例模式有餓漢模式、懶漢模式、靜態內部類、枚舉等方式實現,這些模式的構造方法是私有的,不可繼承
  • 登記式單例 使得單例對繼承開放
  • ThreadLocal 是線程副本形式,能夠保證局部單例,即在各自的線程中是單例的,可是線程與線程之間不保證單例。

\color{#4285f4}{1.特色}

1. 私有構造方法,只能有一個實例。
2. 私有靜態引用指向本身實例,必須是本身在內部建立的惟一實例。
3. 單例類給其它對象提供的都是本身建立的惟一實例
複製代碼

\color{#4285f4}{2.案例}

1. 在計算機系統中,內存、線程、CPU等使用狀況均可以再任務管理器中看到,但始終只能打開一個任務管理器,它在Windows操做系統中是具有惟一性的,由於彈多個框屢次採集數據浪費性能不說,採集數據存在偏差那就有點逗比了不是麼…
2. 每臺電腦只有一個打印機後臺處理程序
3. 線程池的設計通常也是採用單例模式,方便對池中的線程進行控制
複製代碼

\color{#4285f4}{3.注意事項}

1. 實現方式種類較多,有的非線程安全方式的建立須要特別注意,且在使用的時候儘可能根據場景選取較優的,線程安全了還須要去考慮性能問題。
 2. 不適用於變化的對象,若是同一類型的對象老是要在不一樣的用例場景發生變化,單例就會引發數據的錯誤,不能保存彼此的狀態。
 3. 沒有抽象層,擴展有困難。
 4. 職責太重,在必定程度上違背了單一職責原則。
 5. 使用時不能用反射模式建立單例,不然會實例化一個新的對象
複製代碼

3.開啓豔遇

\color{#9932CC}{第一種豔遇:}\color{#34a853}{單一檢查(懶漢)非線程安全}

public class LazyLoadBalancer {

    private static LazyLoadBalancer loadBalancer;
    private List<String> servers = null;

    private LazyLoadBalancer() {
        servers = new ArrayList<>();
    }

    public void addServer(String server) {
        servers.add(server);
    }

    public String getServer() {
        Random random = new Random();
        int i = random.nextInt(servers.size());
        return servers.get(i);
    }

    public static LazyLoadBalancer getInstance() {
        // 第一步:假設T1,T2兩個線程同時進來且知足 loadBalancer == null
        if (loadBalancer == null) {
            // 第二步:那麼 loadBalancer 即會被實例化2次
            loadBalancer = new LazyLoadBalancer();
        }
        return loadBalancer;
    }

    public static void main(String[] args) {
        LazyLoadBalancer balancer1 = LazyLoadBalancer.getInstance();
        LazyLoadBalancer balancer2 = LazyLoadBalancer.getInstance();
        System.out.println("hashCode:"+balancer1.hashCode());
        System.out.println("hashCode:"+balancer2.hashCode());
        balancer1.addServer("Server 1");
        balancer2.addServer("Server 2");
        IntStream.range(0, 5).forEach(i -> System.out.println("轉發至:" + balancer1.getServer()));
    }
}
複製代碼

分析編程

在單線程環境一切正常,balancer1balancer2兩個對象的hashCode如出一轍,由此能夠判斷出堆棧中只有一分內容,不過該代碼塊中存在線程安全隱患,由於缺少競爭條件,多線程環境資源競爭的時候就顯得不太樂觀了,請看上文代碼註釋內容

\color{#9932CC}{第二種豔遇:}\color{#34a853}{無腦上鎖(懶漢)線程安全,性能較差,第一種升級版}

public synchronized static LazyLoadBalancer getInstance() {
    if (loadBalancer == null) {
        loadBalancer = new LazyLoadBalancer();
    }
    return loadBalancer;
}
複製代碼

分析: 設計模式

毫無疑問,知道synchronized關鍵字的都知道,同步方法在鎖沒釋放以前,其它線程都在排隊候着呢,想不安全都不行啊,但在安全的同時,性能方面就顯得短板了,我就初始化一次,你丫的每次來都上個鎖,不累的嗎(不要緊,它是爲了第三種作鋪墊的)..

\color{#9932CC}{第三種豔遇:}\color{#34a853}{雙重檢查鎖(DCL),徹底就是前兩種的結合體啊,有木有,只是將同步方法升級成了同步代碼塊}

//劃重點了 **volatile**
1 private volatile static LazyLoadBalancer loadBalancer;
2
3 public static LazyLoadBalancer getInstance() {
4    if (loadBalancer == null) {
5        synchronized (LazyLoadBalancer.class) {
6            if (loadBalancer == null) {
7               loadBalancer = new LazyLoadBalancer();
8            }
0        }
10    }
11    return loadBalancer;
12 }
複製代碼

假如沒有volatile狀況下產生的問題: 若是第一次檢查loadBalancer不爲null,那麼就不須要執行下面的加鎖和初始化操做。所以,能夠大幅下降synchronized帶來的性能開銷。在線程執行到第4行,代碼讀取到loadBalancer不爲null時,loadBalancer引用的對象有可能尚未完成初始化。在第7行建立了一個對象,這行代碼能夠分解爲以下的3行僞代碼:緩存

memory=allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance=memory; //3:設置instance指向剛分配的內存地址
複製代碼

上面3行代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,若是不瞭解重排序,後文JMM會詳細解釋)。2和3之間重排序以後的執行時序以下安全

memory=allocate(); //1:分配對象的內存空間
instance=memory; //3:設置instance指向剛分配的內存地址,注意此時對象尚未被初始化
ctorInstance(memory); //2:初始化對象
複製代碼

回到示例代碼第7行,若是發生重排序,另外一個併發執行的線程B就有可能在第4行判斷instance不爲null。線程B接下來將訪問instance所引用的對象,但此時這個對象可能尚未被A線程初始化,可能致使NPE。性能優化

分析: 多線程

  1. 假設new LazyLoadBalancer()加載內容過多
  2. 因重排而致使loadBalancer提早不爲空
  3. 正好被其它線程觀察到對象非空直接返回使用 一種罕見的單例空指針忽然來襲
  • 存在問題: 首先咱們必定要清楚,DCL是不能保證線程安全的,稍微瞭解過JVM的就清楚,對比C/C++它始終缺乏一個正式的內存模型,因此爲了提高性能,它還會作一次指令重排操做,這個時候就會致使loadBalancer提早不爲空,正好被其它線程觀察到對象非空直接返回使用(但實際還有部份內容沒加載完成)
  • 解決方案: 用volatile修飾loadBalancer,由於volatile修飾的成員變量能夠確保多個線程都可以順序處理,它會屏蔽JVM指令重排帶來的性能優化。

\color{#9932CC}{第四種豔遇:}\color{#34a853}{Demand Holder,靜態內部類 (懶漢)線程安全,推薦使用}

private LazyLoadBalancer() {}

private static class LoadBalancerHolder {
    //在JVM中 final 對象只會被實例化一次,沒法修改
    private final static LazyLoadBalancer INSTANCE = new LazyLoadBalancer();
}

public static LazyLoadBalancer getInstance() {
    return LoadBalancerHolder.INSTANCE;
}
複製代碼

分析: 併發

在Demand Holder中,咱們在LazyLoadBalancer裏增長一個靜態(static)內部類,在該內部類中建立單例對象,再將 該單例對象經過getInstance()方法返回給外部使用,因爲靜態單例對象沒有做爲LazyLoadBalancer的成員變量直接實例化,類加載時並不會實例化LoadBalancerHolder,所以既能夠實現延遲加載,又能夠保證線程安全,不影響系統性能(居家旅行必備良藥啊)

雙重校驗鎖版,無論性能再如何優越,仍是使用了synchronized修飾符,既然使用了該修飾符,那麼對性能多多少少都會形成一些影響,因而乎Demand Holder誕生,涉及內部類的加載機制,複習一下,代碼以下:dom

package test;

public class OuterTest {

    static {
        System.out.println("load outer class...");
    }

    // 靜態內部類
    static class StaticInnerTest {
        static {
            System.out.println("load static inner class...");
        }

        static void staticInnerMethod() {
            System.out.println("static inner method...");
        }
    }

    public static void main(String[] args) {
        OuterTest outerTest = new OuterTest(); // 此刻其內部類是否也會被加載?
        System.out.println("===========分割線===========");
        OuterTest.StaticInnerTest.staticInnerMethod(); // 調用內部類的靜態方法
    }

}
複製代碼

輸出以下:

load outer class... ===========分割線===========
load static inner class... static inner method 複製代碼

所以,咱們有以下結論:

1. 加載一個類時,其內部類不會同時被加載。
2. 一個類被加載,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被調用時發生。
複製代碼

\color{#9932CC}{第五種豔遇:}\color{#34a853}{懶漢式,  防止反射|序列化|反序列化}

package singleton;

import java.io.Serializable;

public class LazySingleton4 implements Serializable {

    private static boolean initialized = false;

    private LazySingleton4() {
        synchronized (LazySingleton4.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("單例已被破壞");
            }
        }
    }

    static class SingletonHolder {
        private static final LazySingleton4 instance = new LazySingleton4();
    }

    public static LazySingleton4 getInstance() {
        return SingletonHolder.instance;
    }
    
    
    //序列化 防止序列化被破壞單例
    private Object readResolve() {
        return getInstance();
    }
}
複製代碼

分析:

1. 咱們知道 反射能夠建立對象,那咱們由反射的原理即防止反射破壞了單例,所以誕生了如上文的單例

2.在分佈式系統中,有些狀況下你須要在單例類中實現 Serializable 接口。這樣你能夠在文件系統中存儲它的狀態而且在稍後的某一時間點取出,爲了不此問題,咱們須要提供 readResolve() 方法的實現。readResolve()代替了從流中讀取對象。這就確保了在序列化和反序列化的過程當中沒人能夠建立新的實例

爲何反序列化能夠破壞呢?咱們一塊兒來看下ois.readObject()的源碼:

private Object readObject0(boolean unshared) throws IOException {
	...省略
	case TC_OBJECT:
	  return checkResolve(readOrdinaryObject(unshared));
}
-------------------------------------------------------------------
private Object readOrdinaryObject(boolean unshared){
	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 {
	//重點!!!
	//首先isInstantiable()判斷是否能夠初始化
	//若是爲true,則調用newInstance()方法建立對象,這時建立的對象是不走構造函數的,是一個新的對象
            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);
	
	//重點!!!
	//hasReadResolveMethod()會去判斷,咱們的InnerClassSingleton對象中是否有readResolve()方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
	//若是爲true,則執行readResolve()方法,而咱們在本身的readResolve()方法中 直接retrun INSTANCE,因此仍是返回的同一個對象,保證了單例
            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;
}
複製代碼

\color{#9932CC}{第六種豔遇:}\color{#34a853}{枚舉特性(懶漢)線程安全,推薦使用}

enum Lazy {
    INSTANCE;
    private LazyLoadBalancer loadBalancer;

	//枚舉的特性,在JVM中只會被實例化一次
    Lazy() {
        loadBalancer = new LazyLoadBalancer();
    }

    public LazyLoadBalancer getInstance() {
        return loadBalancer;
    }
}
複製代碼

分析:

相比上一種,該方式一樣是用到了JAVA特性:枚舉類保證只有一個實例(即便使用反射機制也沒法屢次實例化一個枚舉量)

\color{#9932CC}{第七種豔遇:}\color{#34a853}{餓漢單例(天生線程安全)}

public class EagerLoadBalancer {
    private final static EagerLoadBalancer INSTANCE = new EagerLoadBalancer();

    private EagerLoadBalancer() {}

    public static EagerLoadBalancer getInstance() {
        return INSTANCE;
    }
}
複製代碼

分析:

利用ClassLoad機制,在加載時進行實例化,同時靜態方法只在編譯期間執行一次初始化,也就只有一個對象。使用的時候已被初始化完畢能夠直接調用,可是相比懶漢模式,它在使用的時候速度最快,但這玩意就像本身挖的坑哭着也得跳,你不用也得初始化一份在內存中佔個坑… 可是寫着簡單~啊~~

\color{#9932CC}{第八種豔遇:}\color{#34a853}{登記式單例}

public class RegistSingleton {
    //用ConcurrentHashMap來維護映射關係,這是線程安全的
    public static final Map<String,Object> REGIST=new ConcurrentHashMap<String, Object>();
    static {
        //把RegistSingleton本身也歸入容器管理
        RegistSingleton registSingleton=new RegistSingleton();
        REGIST.put(registSingleton.getClass().getName(),registSingleton);
    }
    private RegistSingleton(){}
    public static Object getInstance(String className){
        //若是傳入的類名爲空,就返回RegistSingleton實例
        if(className==null)
            className=RegistSingleton.class.getName();
            //若是沒有登記就用反射new一個
        if (!REGIST.containsKey(className)){
            //沒有登記就進入同步塊
            synchronized (RegistSingleton.class){
            //再次檢測是否登記
                if (!REGIST.containsKey(className)){
                    try {
                    //實例化對象
                        REGIST.put(className,Class.forName(className).newInstance());
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        //返回單例
        return REGIST.get(className);
    }
}
複製代碼

來一把測試:

public class Main {
    static CyclicBarrier cyclicBarrier=new CyclicBarrier(1000);
    public static void main(String[] args) {
        for (int i = 0; i <1000 ; i++) {
            int n = i;
            new Thread(()->{
                System.out.println("線程"+ n +"準備就緒");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(RegistSingleton.getInstance("singletonpattern.regist.ClassA"));
            }).start();
        }
    }
}
複製代碼

輸出結果:是線程安全的(ClassA是一個空類,裏面什麼也沒有)

來來 領略一下 Spring的源碼:

public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{    
   /** * 充當了Bean實例的緩存,實現方式和單例註冊表相同 */    
   private final Map singletonCache=new HashMap();    
   public Object getBean(String name)throws BeansException{    
       return getBean(name,null,null);    
   }    
...    
   public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{    
      //對傳入的Bean name稍作處理,防止傳入的Bean name名有非法字符(或則作轉碼) 
      String beanName=transformedBeanName(name);    
      Object bean=null;    
      //手工檢測單例註冊表 
      Object sharedInstance=null;    
      //使用了代碼鎖定同步塊,原理和同步方法類似,可是這種寫法效率更高 
      synchronized(this.singletonCache){    
         sharedInstance=this.singletonCache.get(beanName);    
       }    
      if(sharedInstance!=null){    
         ...    
         //返回合適的緩存Bean實例 
         bean=getObjectForSharedInstance(name,sharedInstance);    
      }else{    
        ...    
        //取得Bean的定義 
        RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);    
         ...    
        //根據Bean定義判斷,此判斷依據一般來自於組件配置文件的單例屬性開關 
        //<bean id="date" class="java.util.Date" scope="singleton"/> 
        //若是是單例,作以下處理 
        if(mergedBeanDefinition.isSingleton()){    
           synchronized(this.singletonCache){    
            //再次檢測單例註冊表 
             sharedInstance=this.singletonCache.get(beanName);    
             if(sharedInstance==null){    
                ...    
               try {    
                  //真正建立Bean實例 
                  sharedInstance=createBean(beanName,mergedBeanDefinition,args);    
                  //向單例註冊表註冊Bean實例 
                   addSingleton(beanName,sharedInstance);    
               }catch (Exception ex) {    
                  ...    
               }finally{    
                  ...    
              }    
             }    
           }    
          bean=getObjectForSharedInstance(name,sharedInstance);    
        }    
       //若是是非單例,即prototpye,每次都要新建立一個Bean實例 
       //<bean id="date" class="java.util.Date" scope="prototype"/> 
       else{    
          bean=createBean(beanName,mergedBeanDefinition,args);    
       }    
}    
...    
   return bean;    
}    
}
複製代碼

分析:

登記式單例實際上維護的是一組單例類的實例,將這些實例存儲到一個Map(登記簿)中,對於已經登記過的單例,則從工廠直接返回,對於沒有登記的,則先登記,然後返回

  1. 使用map實現註冊表;
  2. 使用protect修飾構造方法;

有的時候,咱們不但願在一開始的時候就把一個類寫成單例模式,可是在運用的時候,咱們卻能夠像單例同樣使用他

最典型的例子就是spring,他的默認類型就是單例,spring是如何作到把不是單例的類變成單例呢?

這就用到了登記式單例

其實登記式單例並無去改變類,他所作的就是起到一個登記的做用,若是沒有登記,他就給你登記,並把生成的實例保存起來,下次你要用的時候直接給你。

IOC容器就是作的這個事,你須要就找他去拿,他就能夠很方便的實現Bean的管理。

\color{#9932CC}{第九種豔遇:}\color{#34a853}{ ThreadLocal 局部單例}

public class Singleton {
    
    private Singleton(){}
    
    private static final ThreadLocal<Singleton> threadLocal = 
            new ThreadLocal<Singleton>(){
                @Override
                protected Singleton initialValue(){
                    return new Singleton();
                }
            };
    
    public static Singleton getInstance(){
        return threadLocal.get();
    }
    
}
複製代碼

分析:

這種寫法利用了ThreadLocal的特性,能夠保證局部單例,即在各自的線程中是單例的,可是線程與線程之間不保證單例。

initialValue()通常是用來在使用時進行重寫的,若是在沒有set的時候就調用get,會調用initialValue方法初始化內容。

ThreadLocal會爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式,而ThreadLocal採用了「以空間換時間」的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,即線程隔離,所以能夠同時訪問而互不影響。

\color{#9932CC}{第十種豔遇:}\color{#34a853}{ 使用CAS鎖實現(線程安全)}

/**  * 更加優美的Singleton, 線程安全的  */
public class Singleton {
 /** 利用AtomicReference */
 private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
 /**   * 私有化   */
 private Singleton(){
 }
 /**   * 用CAS確保線程安全   */
 public static final Singleton getInstance(){
  for (;;) {
   Singleton current = INSTANCE.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) {
                return current;
            }
        }
 }
 
 public static void main(String[] args) {
  Singleton singleton1 = Singleton.getInstance();
  Singleton singleton2 = Singleton.getInstance();
     System.out.println(singleton1 == singleton2);
 }
}

複製代碼

分析:


CAS 是線程安全的,使用了無鎖編程. 這種方式當在大量線程去獲取實例的時候,會形成CPU的激情燃燒~

4.總結

本文給出了多個版本的單例模式,供咱們在項目中使用。實際上,咱們在實際項目中通常從豔遇4、5、六中,根據實際狀況三選一便可。 最後,但願你們有所收穫。

相關文章
相關標籤/搜索