文章目錄
單例模式的定義與特點
單例(Singleton
)模式的定義:
是指確保一個類在任何情況下都絕對只有一個實例,隱藏其所有的構造方法,並提供一個全局訪問點。屬於創建型模式。
單例模式有 3 個特點:
1.單例類只有一個實例對象;
2.該單例對象必須由單例類自行創建;
3.單例類對外提供一個訪問該單例的全局訪問點;
單例模式的結構
單例模式是設計模式中最簡單的模式之一,但也是面試時常被問到的。
通常,普通類的構造函數是公有的,外部類可以通過“new 構造函數()”來生成多個實例。但是,如果將類的構造函數設爲私有的,外部類就無法調用該構造函數,也就無法生成多個實例。這時該類自身必須定義一個靜態私有實例,並向外提供一個靜態的公有函數用於創建或獲取該靜態私有實例。
單例模式的主要角色如下:
單例類:包含一個實例且能自行創建這個實例的類。
訪問類:使用單例的類。
單例模式的實現
說起單例模式的實現,我們首先會想到的就是“餓漢式"和"懶漢式"下面就詳細的講講。
餓漢式單例
特點:在單例類首次加載的時候就創建實例。
優點:沒有任何鎖,執行效率高,性能高。
缺點:在某些情況下,可能會造成內存的浪費,因爲不管你用不用,它都會在類加載時創建一個對象。
常見的餓漢式單例寫法:
/**
* 常見的餓漢式單例寫法
*/
public class HungrySingleton {
//類加載時就創建HungrySingleton這個實例對象
private static final HungrySingleton hungrySingleton = new HungrySingleton();
//私有化構造函數
private HungrySingleton(){}
//提供全局訪問點
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
靜態代碼塊寫法:
/**
* 靜態代碼塊寫法
* 看來更有逼格,其實和上邊的差不多
*/
public class HungryStaticSingleton {
//類加載時就創建HungrySingleton這個實例對象
private static final HungryStaticSingleton hungryStaticSingleton;
static {
hungryStaticSingleton = new HungryStaticSingleton();
}
//私有化構造函數
private HungryStaticSingleton(){}
//提供全局訪問點
public static HungryStaticSingleton getInstance(){
return hungryStaticSingleton;
}
}
因爲餓漢式的缺點,爲了避免內存造成不必要的浪費,所以出現懶漢式單例。
懶漢式單例
特點:在被外部類調用時才創建實例,解決了餓漢式的內存浪費。
優點:節省內存,減少不必要的內存浪費。
1.最簡單的懶漢式寫法:
缺點:線程不安全
/**
* 最簡單的懶漢式
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton;
//私有化構造函數
private LazySimpleSingleton(){}
//提供全局訪問點,用的時候才創建實例
public static LazySimpleSingleton getInstance(){
if (lazySimpleSingleton == null){
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
線程破壞懶漢式單例的事故現場:
測試代碼:
public class LazySimpleSingletonTest {
@Test
public void test1(){
new Thread(()->{
System.out.println(LazySimpleSingleton.getInstance());}
).start();
new Thread(()->{
System.out.println(LazySimpleSingleton.getInstance());}
).start();
}
}
多次運行發現兩條線程會有可能會創建出不同的實例對象(如下圖),這就違背了單例模式的定義了。
於是產生了懶漢式的第二種寫法(加鎖)
2.懶漢式第二種寫法(加鎖):
解決了線程不安全問題。
缺點:性能低,加鎖後導致並行變串行
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton;
//私有化構造函數
private LazySimpleSingleton(){}
//提供全局訪問點
public static LazySimpleSingleton getInstance(){
//加鎖,解決線程不安全問題
synchronized (LazySimpleSingleton.class) {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
return lazySimpleSingleton;
}
}
3.懶漢式第三種寫法(雙重檢查鎖):
解決了線程不安全問題的同時,也解決了性能低的問題。
注意:變量lazySimpleSingleton
加volatile
修飾。因爲線程中是存在指令重排序的問題,變量定義的時候會創建一塊內存,而創建實例的時候也會創建一塊內存,變量要指向創建實例的內存。線程運行中會導致這些順序發生變化,也就是指令重排序的問題。所以加volatile
關鍵字修飾保證有序性。
缺點:兩個if
判斷導致代碼看起來不是那麼的優雅,可讀性不高。
public class LazySimpleSingleton {
private static volatile LazySimpleSingleton lazySimpleSingleton;
//私有化構造函數
private LazySimpleSingleton(){}
//提供全局訪問點
public static LazySimpleSingleton getInstance(){
//檢查是否需要加鎖阻塞
if (lazySimpleSingleton == null){
synchronized (LazySimpleSingleton.class) {
//檢查是否需要創建新的實例
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
4.懶漢式第四種寫法(靜態內部類):
特點:利用Java語法的特點,寫法優雅,性能高,懶加載避免了內存浪費。看似已經非常完美啦。
解釋:因爲它創建實例是在靜態內部類中,而靜態內部類在被使用到的時候纔會被加載,所以說避免了內存浪費。
/**
* 懶漢式靜態內部類寫法
*/
public class LazyStaticInnerClassSimpleSingleton {
//私有化構造函數
private LazyStaticInnerClassSimpleSingleton(){}
//全局訪問點
private static LazyStaticInnerClassSimpleSingleton getInstance(){
return StaticInner.Instance;
}
//靜態內部類,創建實例
private static class StaticInner{
private static final LazyStaticInnerClassSimpleSingleton Instance = new LazyStaticInnerClassSimpleSingleton();
}
}
上邊說到的
懶漢式靜態內部類寫法
,看似已經很完美了。其實不然,上邊所說到的所有單例寫法其實有兩個共同的缺點:
1.能夠被反序列化破壞
2.能夠被反射破壞
反序列化破壞單例的事故現場
序列化:
就是把內存中對象的狀態轉化爲字節碼的形式,把字節碼通過IO輸出流寫到磁盤上,持久化保存下來。
反序列化:
就是將持久化的字節碼內容,通過IO輸入流讀取到內存中,轉化成一個實例對象。
這裏咱們就拿常見的餓漢式單例寫法
來演示:
測試代碼:
@Test
public void test(){
HungrySingleton instance1 = HungrySingleton.getInstance();
try {
//將instance1對象 從內存寫入到硬盤
FileOutputStream fos = new FileOutputStream("HungrySingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance1);
oos.flush();
oos.close();
//從硬盤讀取到內存
FileInputStream fis = new FileInputStream("HungrySingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Object instance2 = ois.readObject();
ois.close();
//比較instance1和instance2是否是同一個實例
System.out.println(instance1 );
System.out.println(instance2);
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
運行結果
: 可以發現,反序列化出來的對象實例和全局訪問點獲取出來的對象實例不相同,即違背了單例原則。
解決辦法
:其實很簡單,只需要在單例類中加一個方法:
再次看運行結果
:
反射破壞單例的事故現場
這裏咱們就拿懶漢式靜態內部類寫法
來演示:
測試代碼:
public class LazyStaticInnerClassSimpleSingletonTest {
@Test
public void test() throws Exception {
Class <?> clazz = LazyStaticInnerClassSimpleSingleton.class;
//獲取所有無參構造器(包括私有)
Constructor<?> constructor = clazz.getDeclaredConstructor(null);
//強制訪問
constructor.setAccessible(true);
//創建實例
Object o1 = constructor.newInstance();
Object o2 = constructor.newInstance();
//比較是否是同一個實例對象
System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);
}
}
運行結果: 可以發現,反射直接就繞過了單例類提供的唯一訪問點。這就是反射破壞單例的事故現場。
解決辦法
:可以在無參構造方法裏做個判斷,拋異常。
再來看運行結果
:
是不是反射就創建不了對象了。但是,但是,本來很優雅的代碼,結果你在構造方法中拋個異常,幾個意思?感覺就很奇怪,這就不能忍了吧。且看下回分析。。。下邊分析啊,新的寫法又要來了,乃金剛不壞之身:序列化破壞不了它,反射也破壞不了它。
枚舉式單例
堪稱完美的單例啊,就下邊一個缺點。。。
優點:優雅的代碼,線程安全且避免了序列化和反射的破壞(因爲反射是創建不了枚舉對象的,直接會報錯,待會看源碼)。
缺點:類初始化時,就創建了這個枚舉,在某些情況下可能造成內存浪費。
/**
* 枚舉式單例
*/
public enum EnumSingleton {
INSTANCE; //這個就是實例
//定義一個屬性,並提供get,set方法
private String name;
public String getName() {return name;}
public void setName(String name) {this.name = name;}
//全局訪問點
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
測試代碼:
public class EnumSingletonTest {
@Test
public void test(){
//創建兩個實例對象
EnumSingleton instance1 = EnumSingleton.getInstance();
EnumSingleton instance2 = EnumSingleton.getInstance();
/**
* 比較兩個實例對象是否是同一個
* 如果是同一個對象實例,那麼返回true,並且用instance2可以獲取到instance1設置的名字
*/
System.out.println(instance1==instance2);
instance1.setName("枚舉單例");//用instance1 設置名字
System.out.println(instance2.getName());//用instance2來獲取名字
}
}
運行結果:
聊一聊爲什麼枚舉單例不會被反射破壞:
看一段源碼:
找到Constructor
這個類中的第416
行,判斷如果這個類是被ENUM
(枚舉)修飾的,那麼就直接拋出Cannot reflectively create enum objects
。
靠,爲啥人家拋異常就行,咱們在無參構造裏拋個異常就不優雅了啊? 別問爲啥,人家是官方,人家牛掰!
ThreadLocal單例
特點:保證線程內部的全局唯一,且天生線程安全。
注意:是線程內部全局唯一,也就是說,一個線程一個單例實例,線程之間是相互隔離的。
/**
* ThreadLocal單例寫法
*/
public class ThreadLocalSingleton {
//私有化無參構造
private ThreadLocalSingleton(){}
//交給ThreadLocal創建實例
private static final ThreadLocal<ThreadLocalSingleton> instance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
//全局訪問點
private static ThreadLocalSingleton getInstance(){
return instance.get();
}
}
測試代碼:
public class ThreadLocalSingletonTest {
@Test
public void test(){
ThreadLocalSingleton mainInstance1 = ThreadLocalSingleton.getInstance();//主線程實例1
ThreadLocalSingleton mainInstance2 = ThreadLocalSingleton.getInstance();//主線程實例2
System.out.println("主線程兩個實例:");
System.out.println(mainInstance1);
System.out.println(mainInstance2);
new Thread(()->{
ThreadLocalSingleton thread1Instance = ThreadLocalSingleton.getInstance();//thread1線程實例
System.out.println("thread1線程實例:"+thread1Instance);
}, "thread1").start();
new Thread(()->{
ThreadLocalSingleton thread2Instance = ThreadLocalSingleton.getInstance();//thread2線程實例
System.out.println("thread2線程實例是否相同:"+thread2Instance);
}, "thread2").start();
}
}
運行結果:
可以看出來,主線程創建的兩個實例是相同的,所以說:線程內部創建出來的實例是相同的
但是,每個線程創建出來的實例是不同的,所以說:一個線程一個單例實例,線程之間是相互隔離的。
單例模式總結
優點:
1.在內存中只有一個實例,減少內存開銷
2.可以避免對資源的多重佔用
3.設置全局訪問點,嚴格控制訪問
缺點:
1.沒有接口,擴展困難,如果要擴展,只有修改代碼,沒有其他途徑
重點:
1.私有化構造器
2.保證線程安全
3.延遲加載
4.防止被序列化和反序列化破壞
5.防止被反射破壞