就算你沒有用到過其他的設計模式,但是單例模式你肯定接觸過,比如,Spring 中 bean 默認就是單例模式的,所有用到這個 bean 的實例其實都是同一個。
單例模式的使用場景
什么是單例模式呢,單例模式(Singleton)又叫單態模式,它出現目的是為了保證一個類在系統中只有一個實例,並提供一個訪問它的全局訪問點。從這點可以看出,單例模式的出現是為了可以保證系統中一個類只有一個實例而且該實例又易於外界訪問,從而方便對實例個數的控制並節約系統資源而出現的解決方案。
使用單例模式當然是有原因,有好處的了。在下面幾個場景中適合使用單例模式:
1、有頻繁實例化然后銷毀的情況,也就是頻繁的 new 對象,可以考慮單例模式;
2、創建對象時耗時過多或者耗資源過多,但又經常用到的對象;
3、頻繁訪問 IO 資源的對象,例如數據庫連接池或訪問本地文件;
下面舉幾個例子來說明一下:
1、網站在線人數統計;
其實就是全局計數器,也就是說所有用戶在相同的時刻獲取到的在線人數數量都是一致的。要實現這個需求,計數器就要全局唯一,也就正好可以用單例模式來實現。當然這里不包括分布式場景,因為計數是存在內存中的,並且還要保證線程安全。下面代碼是一個簡單的計數器實現。
public class Counter {
private static class CounterHolder{
private static final Counter counter = new Counter();
}
private Counter(){
System.out.println("init...");
}
public static final Counter getInstance(){
return CounterHolder.counter;
}
private AtomicLong online = new AtomicLong();
public long getOnline(){
return online.get();
}
public long add(){
return online.incrementAndGet();
}
}
2、配置文件訪問類;
項目中經常需要一些環境相關的配置文件,比如短信通知相關的、郵件相關的。比如 properties 文件,這里就以讀取一個properties 文件配置為例,如果你使用的 Spring ,可以用 @PropertySource 注解實現,默認就是單例模式。如果不用單例的話,每次都要 new 對象,每次都要重新讀一遍配置文件,很影響性能,如果用單例模式,則只需要讀取一遍就好了。以下是文件訪問單例類簡單實現:
public class SingleProperty {
private static Properties prop;
private static class SinglePropertyHolder{
private static final SingleProperty singleProperty = new SingleProperty();
}
/**
* config.properties 內容是 test.name=kite
*/
private SingleProperty(){
System.out.println("構造函數執行");
prop = new Properties();
InputStream stream = SingleProperty.class.getClassLoader()
.getResourceAsStream("config.properties");
try {
prop.load(new InputStreamReader(stream, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SingleProperty getInstance(){
return SinglePropertyHolder.singleProperty;
}
public String getName(){
return prop.get("test.name").toString();
}
public static void main(String[] args){
SingleProperty singleProperty = SingleProperty.getInstance();
System.out.println(singleProperty.getName());
}
}
3、數據庫連接池的實現,也包括線程池。為什么要做池化,是因為新建連接很耗時,如果每次新任務來了,都新建連接,那對性能的影響實在太大。所以一般的做法是在一個應用內維護一個連接池,這樣當任務進來時,如果有空閑連接,可以直接拿來用,省去了初始化的開銷。所以用單例模式,正好可以實現一個應用內只有一個線程池的存在,所有需要連接的任務,都要從這個連接池來獲取連接。如果不使用單例,那么應用內就會出現多個連接池,那也就沒什么意義了。如果你使用 Spring 的話,並集成了例如 druid 或者 c3p0 ,這些成熟開源的數據庫連接池,一般也都是默認以單例模式實現的。
單例模式的實現方法
如果你在書上或者網站上搜索單例模式的實現,一般都會介紹5、6中方式,其中有一些隨着 Java 版本的升高,以及多線程技術的使用變得不那么實用了,這里就介紹兩種即高效,而且又是線程安全的方式。
1. 靜態內部類方式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種寫法仍然使用 JVM 本身機制保證了線程安全問題,由於 SingletonHolder 是私有的,除了 getInstance() 方法外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。上面的兩個例子就是用這種方式實現的。
2. 枚舉方式
public enum SingleEnum {
INSTANCE;
SingleEnum(){
System.out.println("構造函數執行");
}
public String getName(){
return "singleEnum";
}
public static void main(String[] args){
SingleEnum singleEnum = SingleEnum.INSTANCE;
System.out.println(singleEnum.getName());
}
}
我們可以通過 SingleEnum.INSTANCE 來訪問實例。而且創建枚舉默認就是線程安全的,並且還能防止反序列化導致重新創建新的對象。
靜態塊
什么是靜態塊呢。
1、它是隨着類的加載而執行,只執行一次,並優先於主函數。具體說,靜態代碼塊是由類調用的。類調用時,先執行靜態代碼塊,然后才執行主函數的;
2、靜態代碼塊其實就是給類初始化的,而構造代碼塊是給對象初始化的;
3、靜態代碼塊中的變量是局部變量,與普通函數中的局部變量性質沒有區別;
4、一個類中可以有多個靜態代碼塊;
他的寫法是這樣的:
static {
System.out.println("static executed");
}
來看一下下面這個完整的實例:
public class SingleStatic {
static {
System.out.println("static 塊執行中...");
}
{
System.out.println("構造代碼塊 執行中...");
}
public SingleStatic(){
System.out.println("構造函數 執行中");
}
public static void main(String[] args){
System.out.println("main 函數執行中");
SingleStatic singleStatic = new SingleStatic();
}
}
他的執行結果是這樣的:
static 塊執行中...
main 函數執行中
構造代碼塊 執行中...
構造函數 執行中
從中可以看出他們的執行順序分別為:
1、靜態代碼塊
2、main 函數
3、構造代碼塊
4、構造函數
利用靜態代碼塊只在類加載的時候執行,並且只執行一次這個特性,也可以用來實現單例模式,但是不是懶加載,也就是說每次類加載就會主動觸發實例化。
除此之外,不考慮單例的情況,利用靜態代碼塊的這個特性,可以實現其他的一些功能,例如上面提到的配置文件加載的功能,可以在類加載的時候就讀取配置文件的內容,相當於一個預加載的功能,在使用的時候可以直接拿來就用。