單例模式,真不簡單

前言

單例模式無論在我們面試,還是日常工作中,都會面對的問題。但很多單例模式的細節,值得我們深入探索一下。

這篇文章透過單例模式,串聯了多方面基礎知識,非常值得一讀。

1 什麼是單例模式?

單例模式是一種非常常用的軟件設計模式,它定義是單例對象的類只能允許一個實例存在

該類負責創建自己的對象,同時確保只有一個對象被創建。一般常用在工具類的實現或創建對象需要消耗資源的業務場景。

單例模式的特點:

我們先用一個簡單示例瞭解一下單例模式的用法。

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
    
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

打印結果:

1639705018
1639705018

我們看到兩次獲取 SimpleSingleton 實例的 hashCode 是一樣的,說明兩次調用獲取到的是同一個對象。

可能很多朋友平時工作當中都是這麼用的,但我要說這段代碼是有問題的,你會相信嗎?

不信,我們一起往下看。

2 餓漢和懶漢模式

在介紹單例模式的時候,必須要先介紹它的兩種非常著名的實現方式:餓漢模式懶漢模式

2.1 餓漢模式

實例在初始化的時候就已經建好了,不管你有沒有用到,先建好了再說。具體代碼如下:

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

餓漢模式,其實還有一個變種:

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE;
    static {
       INSTANCE = new SimpleSingleton();
    }

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

使用靜態代碼塊的方式實例化 INSTANCE 對象。

使用餓漢模式的好處是:沒有線程安全的問題,但帶來的壞處也很明顯。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一開始就實例化對象了,如果實例化過程非常耗時,並且最後這個對象沒有被使用,不是白白造成資源浪費嗎?

還真是啊。

這個時候你也許會想到,不用提前實例化對象,在真正使用的時候再實例化不就可以了?

這就是我接下來要介紹的:懶漢模式

2.2 懶漢模式

顧名思義就是實例在用到的時候纔去創建,“比較懶”,用的時候纔去檢查有沒有實例,如果有則返回,沒有則新建。具體代碼如下:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

示例中的 INSTANCE 對象一開始是空的,在調用 getInstance 方法纔會真正實例化。

嗯,不錯不錯。但這段代碼還是有問題。

2.3 synchronized 關鍵字

上面的代碼有什麼問題?

答:假如有多個線程中都調用了 getInstance 方法,那麼都走到 if (INSTANCE == null) 判斷時,可能同時成立,因爲 INSTANCE 初始化時默認值是 null。這樣會導致多個線程中同時創建 INSTANCE 對象,即 INSTANCE 對象被創建了多次,違背了只創建一個 INSTANCE 對象的初衷。

那麼,要如何改進呢?

答:最簡單的辦法就是使用synchronized關鍵字。

改進後的代碼如下:

public class SimpleSingleton3 {
    private static SimpleSingleton3 INSTANCE;

    private SimpleSingleton3() {
    }

    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}

在 getInstance 方法上加synchronized關鍵字,保證在併發的情況下,只有一個線程能創建 INSTANCE 對象的實例。

這樣總可以了吧?

答:不好意思,還是有問題。

有什麼問題?

答:使用 synchronized 關鍵字會消耗 getInstance 方法的性能,我們應該判斷當 INSTANCE 爲空時才加鎖,如果不爲空不應該加鎖,需要直接返回。

這就需要使用下面要說的雙重檢查鎖了。

2.4 餓漢和懶漢模式的區別

but,在介紹雙重檢查鎖之前,先插播一個朋友們可能比較關心的話題:餓漢模式 和 懶漢模式 各有什麼優缺點?

好了,下面可以安心的看看雙重檢查鎖,是如何保證性能的,同時又保證單例的。

3 雙重檢查鎖

雙重檢查鎖顧名思義會檢查兩次:在加鎖之前檢查一次是否爲空,加鎖之後再檢查一次是否爲空。

那麼,它是如何實現單例的呢?

3.1 如何實現單例?

具體代碼如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

在加鎖之前判斷是否爲空,可以確保 INSTANCE 不爲空的情況下,不用加鎖,可以直接返回。

爲什麼在加鎖之後,還需要判斷 INSTANCE 是否爲空呢?

答:是爲了防止在多線程併發的情況下,只會實例化一個對象。

比如:線程 a 和線程 b 同時調用 getInstance 方法,假如同時判斷 INSTANCE 都爲空,這時會同時進行搶鎖。

假如線程 a 先搶到鎖,開始執行 synchronized 關鍵字包含的代碼,此時線程 b 處於等待狀態。

線程 a 創建完新實例了,釋放鎖了,此時線程 b 拿到鎖,進入 synchronized 關鍵字包含的代碼,如果沒有再判斷一次 INSTANCE 是否爲空,則可能會重複創建實例。

所以需要在 synchronized 前後兩次判斷。

不要以爲這樣就完了,還有問題呢?

3.2 volatile 關鍵字

上面的代碼還有啥問題?

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

getInstance 方法的這段代碼,我是按 1、2、3、4、5 這種順序寫的,希望也按這個順序執行。

但是 java 虛擬機實際上會做一些優化,對一些代碼指令進行重排。重排之後的順序可能就變成了:1、3、2、4、5,這樣在多線程的情況下同樣會創建多次實例。重排之後的代碼可能如下:

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

原來如此,那有什麼辦法可以解決呢?

答:可以在定義 INSTANCE 是加上volatile關鍵字。具體代碼如下:

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

    public static SimpleSingleton7 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton7.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton7();
                }
            }
        }
        return INSTANCE;
    }
}

volatile關鍵字可以保證多個線程的可見性,但是不能保證原子性。同時它也能禁止指令重排。

雙重檢查鎖的機制既保證了線程安全,又比直接上鎖提高了執行效率,還節省了內存空間。

除了上面的單例模式之外,還有沒有其他的單例模式?

4 靜態內部類

靜態內部類顧名思義是通過靜態的內部類來實現單例模式的。

那麼,它是如何實現單例的呢?

4.1 如何實現單例模式?

具體代碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

我們看到在 SimpleSingleton5 類中定義了一個靜態的內部類 Inner。在 SimpleSingleton5 類的 getInstance 方法中,返回的是內部類 Inner 的實例 INSTANCE 對象。

只有在程序第一次調用 getInstance 方法時,虛擬機才加載 Inner 並實例化 INSTANCE 對象。

java 內部機制保證了,只有一個線程可以獲得對象鎖,其他的線程必須等待,保證對象的唯一性。

4.2 反射漏洞

上面的代碼看似完美,但還是有漏洞。如果其他人使用反射,依然能夠通過類的無參構造方式創建對象。例如:

Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
try {
    SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
    System.out.println(newInstance == SimpleSingleton5.getInstance());
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

上面代碼打印結果是 false。

由此看出,通過反射創建的對象,跟通過 getInstance 方法獲取的對象,並非同一個對象,也就是說,這個漏洞會導致 SimpleSingleton5 非單例。

那麼,要如何防止這個漏洞呢?

答:這就需要在無參構造方式中判斷,如果非空,則拋出異常了。

改造後的代碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
        if(Inner.INSTANCE != null) {
           throw new RuntimeException("不能支持重複實例化");
       }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
        }
    }

}

如果此時,你認爲這種靜態內部類,實現單例模式的方法,已經完美了。

那麼,我要告訴你的是,你錯了,還有漏洞。。。

4.3 反序列化漏洞

衆所周知,java 中的類通過實現Serializable接口,可以實現序列化。

我們可以把類的對象先保存到內存,或者某個文件當中。後面在某個時刻,再恢復成原始對象。

具體代碼如下:

public class SimpleSingleton5 implements Serializable {

    private SimpleSingleton5() {
        if (Inner.INSTANCE != null) {
            throw new RuntimeException("不能支持重複實例化");
        }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    private static void writeFile() {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
            fos = new FileOutputStream(new File("test.txt"));
            oos = new ObjectOutputStream(fos);
            oos.writeObject(simpleSingleton5);
            System.out.println(simpleSingleton5.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private static void readFile() {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            ois = new ObjectInputStream(fis);
            SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();

            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        writeFile();
        readFile();
    }
}

運行之後,發現序列化和反序列化後對象的 hashCode 不一樣:

189568618
793589513

說明,反序列化時創建了一個新對象,打破了單例模式對象唯一性的要求。

那麼,如何解決這個問題呢?

答:重新 readResolve 方法。

在上面的實例中,增加如下代碼:

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

運行結果如下:

290658609
290658609

我們看到序列化和反序列化實例對象的 hashCode 相同了。

做法很簡單,只需要在 readResolve 方法中,每次都返回唯一的 Inner.INSTANCE 對象即可。

程序在反序列化獲取對象時,會去尋找 readResolve() 方法。

好了,到這來終於把坑都踩完了。

還是費了不少勁。

不過,我偷偷告訴你一句,其實還有更簡單的方法,哈哈哈。

納尼。。。

5 枚舉

其實在 java 中枚舉就是天然的單例,每一個實例只有一個對象,這是 java 底層內部機制保證的。

簡單的用法:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    public void doSamething() {
        System.out.println("doSamething");
    }
}

在調用的地方:

public class SimpleSingleton7Test {

    public static void main(String[] args) {
        SimpleSingleton7.INSTANCE.doSamething();
    }
}

在枚舉中實例對象 INSTANCE 是唯一的,所以它是天然的單例模式。

當然,在枚舉對象唯一性的這個特性,還能創建其他的單例對象,例如:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    private Student instance;
    
    SimpleSingleton7() {
       instance = new Student();
    }
    
    public Student getInstance() {
       return instance;
    }
}

class Student {
}

jvm 保證了枚舉是天然的單例,並且不存在線程安全問題,此外,還支持序列化。

在 java 大神 Joshua Bloch 的經典書籍《Effective Java》中說過:

單元素的枚舉類型已經成爲實現 Singleton 的最佳方法。

6 多例模式

我們之前聊過的單例模式,都只會產生一個實例。但它其實還有一個變種,也就是我們接下來要聊的:多例模式

多例模式顧名思義,它允許創建多個實例。但它的初衷是爲了控制實例的個數,其他的跟單例模式差不多。

具體實現代碼如下:

public class SimpleMultiPattern {
    //持有自己類的引用
    private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
    private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();

    //私有的構造方法
    private SimpleMultiPattern() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleMultiPattern getInstance(int type) {
        if(type == 1) {
          return INSTANCE1;
        }
        return INSTANCE2;
    }
}

爲了看起來更直觀,我把一些額外的安全相關代碼去掉了。

有些朋友可能會說:既然多例模式也是爲了控制實例數量,那我們常見的池技術,比如:數據庫連接池,是不是通過多例模式實現的?

答:不,它是通過享元模式實現的。

那麼,多例模式和享元模式有什麼區別?

7 真實使用場景

最後,跟大家一起聊聊,單例模式的一些使用場景。我們主要看看在 java 的框架中,是如何使用單例模式,給有需要的朋友一個參考。

7.1 Runtime

jdk 提供了Runtime類,我們可以通過這個類獲取系統的運行狀態。

比如可以通過它獲取 cpu 核數:

int availableProcessors = Runtime.getRuntime().availableProcessors();

Runtime類的關鍵代碼如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    ...
}

從上面的代碼我們可以看出,這是一個單例模式,並且是餓漢模式。

但根據文章之前講過的一些理論知識,你會發現 Runtime 類的這種單例模式實現方式,顯然不太好。實例對象既沒用final關鍵字修飾,也沒考慮對象實例化的性能消耗問題。

不過它的優點是實現起來非常簡單。

7.2 NamespaceHandlerResolver

spring 提供的 DefaultNamespaceHandlerResolver 是爲需要初始化默認命名空間處理器,是爲了方便後面做標籤解析用的。

它的關鍵代碼如下:

@Nullable
private volatile Map<String, Object> handlerMappings;

private Map<String, Object> getHandlerMappings() {
  Map<String, Object> handlerMappings = this.handlerMappings;
  if (handlerMappings == null) {
   synchronized (this) {
    handlerMappings = this.handlerMappings;
    if (handlerMappings == null) {
     if (logger.isDebugEnabled()) {
      logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
     }
     try {
      Properties mappings =
        PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
      if (logger.isDebugEnabled()) {
       logger.debug("Loaded NamespaceHandler mappings: " + mappings);
      }
      handlerMappings = new ConcurrentHashMap<>(mappings.size());
      CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
      this.handlerMappings = handlerMappings;
     }
     catch (IOException ex) {
      throw new IllegalStateException(
        "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
     }
    }
   }
  }
  return handlerMappings;
 }

我們看到它使用了雙重檢測鎖,並且還定義了一個局部變量 handlerMappings,這是非常高明之處。

使用局部變量相對於不使用局部變量,可以提高性能。主要是由於 volatile 變量創建對象時需要禁止指令重排序,需要一些額外的操作。

7.3 LogFactory

mybatis 提供LogFactory類是爲了創建日誌對象,根據引入的 jar 包,決定使用哪種方式打印日誌。具體代碼如下:

public final class LogFactory {

  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}

這段代碼非常經典,但它卻是一個不走尋常路的單例模式。因爲它創建的實例對象,可能存在多種情況,根據引入不同的 jar 包,加載不同的類創建實例對象。如果有一個創建成功,則用它作爲整個類的實例對象。

這裏有個非常巧妙的地方是:使用了很多 tryImplementation 方法,方便後面進行擴展。不然要寫很多,又臭又長的 if...else 判斷。

此外,它跟常規的單例模式的區別是,LogFactory 類中定義的實例對象是 Log 類型,並且 getLog 方法返回的參數類型也是 Log,不是 LogFactory。

最關鍵的一點是:getLog 方法中是通過構造器的 newInstance 方法創建的實例對象,每次請求 getLog 方法都會返回一個新的實例,它其實是一個多例模式。

7.4 ErrorContext

mybatis 提供ErrorContext類記錄了錯誤信息的上下文,方便後續處理。

那麼它是如何實現單例模式的呢?關鍵代碼如下:

public class ErrorContext {
  ...
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
  
  private ErrorContext() {
  }
  
  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
  ...
}

我們可以看到,ErrorContext 跟傳統的單例模式不一樣,它改良了一下。它使用了餓漢模式,並且使用ThreadLocal,保證每個線程中的實例對象是單例的。這樣看來,ErrorContext 類創建的對象不是唯一的,它其實也是多例模式的一種。

7.5 spring 的單例

以前在 spring 中要定義一個 bean,需要在 xml 文件中做如下配置:

<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">

在 bean 標籤上有個scope屬性,我們可以通過指定該屬性控制 bean 實例是單例的,還是多例的。如果值爲singleton,代表是單例的。當然如果該參數不指定,默認也是單例的。如果值爲prototype,則代表是多例的。

在 spring 的AbstractBeanFactory類的doGetBean方法中,有這樣一段代碼:

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      return createBean(beanName, mbd, args);
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
    Object prototypeInstance = createBean(beanName, mbd, args);
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
    ....
}

這段代碼我爲了好演示,看起來更清晰,我特地簡化過的。它的主要邏輯如下:

  1. 判斷如果 scope 是 singleton,則調用 getSingleton 方法獲取實例。

  2. 如果 scope 是 prototype,則直接創建 bean 實例,每次會創建一個新實例。

  3. 如果 scope 是其他值,則允許我們自定 bean 的創建過程。

其中 getSingleton 方法主要代碼如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
  Assert.notNull(beanName, "Bean name must not be null");
  synchronized (this.singletonObjects) {
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null) {
          singletonObject = singletonFactory.getObject();
         if (newSingleton) {
           addSingleton(beanName, singletonObject);
        }
   }
   return singletonObject;
  }
}

有個關鍵的 singletonObjects 對象,其實是一個 ConcurrentHashMap 集合:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

getSingleton 方法的主要邏輯如下:

  1. 根據 beanName 先從 singletonObjects 集合中獲取 bean 實例。

  2. 如果 bean 實例不爲空,則直接返回該實例。

  3. 如果 bean 實例爲空,則通過 getObject 方法創建 bean 實例,然後通過 addSingleton 方法,將該 bean 實例添加到 singletonObjects 集合中。

  4. 下次再通過 beanName 從 singletonObjects 集合中,就能獲取到 bean 實例了。

在這裏 spring 是通過 ConcurrentHashMap 集合來保證對象的唯一性。

最後留給大家幾個小問題思考一下:

  1. 多例模式 和 多對象模式有什麼區別?

  2. java 框架中有些單例模式用的不規範,我要參考不?

  3. spring 的單例,只是結果是單例的,但完全沒有遵循單例模式的固有寫法,它也算是單例模式嗎?

歡迎大家給我留言,說出你心中的答案。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YRDd0R33CcjF_hmiSQr0aQ