文章目錄
- 單例模式詳解
- 0.概述
- 1.餓漢式
- 1.1 餓漢式單例實作
- 1.2 破壞單例的幾種情況
- 1.3 預防單例的破壞
- 2.列舉餓漢式
- 2.1 列舉單例實作
- 2.2 破壞單例
- 3.懶漢式
- 4.雙檢鎖懶漢式
- 5.內部類懶漢式
- 6.JDK中單例的體現
單例模式詳解
0.概述
為什么要使用單例模式?
在我們的系統中,有一些物件其實我們只需要一個,比如說:執行緒池、快取、對話框、注冊表、日志物件、充當列印機、顯卡等設備驅動程式的物件,事實上,這一類物件只能有一個實體,如果制造出多個實體就可能會導致一些問題的產生,比如:程式的行為例外、資源使用過量、或者不一致性的結果,因此這里需要用到單例模式
使用單例模式的好處?
- 對于頻繁使用的物件,可以省略創建物件所花費的時間,這對于那些重量級物件而言,是非常可觀的一筆系統開銷
- 由于 new 操作的次數減少,因而對系統記憶體的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間
1.餓漢式
1.1 餓漢式單例實作
實體會提前創建:
/**
* 餓漢式
*
* @author xppll
* @date 2021/12/24 21:21
*/
public class Singleton1 implements Serializable {
//構造私有
private Singleton1() {
System.out.println("private Singleton1()");
}
//唯一實體
private static final Singleton1 INSTANCE = new Singleton1();
//獲得實體方法
public static Singleton1 getINSTANCE() {
return INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
測驗:
/**
* @author xppll
* @date 2021/12/24 21:28
*/
public class TestSingleton {
public static void main(String[] args) {
//觸發Singleton1類的初始化,會為類的靜態變數賦予正確的初始值,單例物件就會被創建!
Singleton1.otherMethod();
System.out.println("-----------------------------------");
System.out.println(Singleton1.getINSTANCE());
System.out.println(Singleton1.getINSTANCE());
}
}
//輸出:
private Singleton1()
otherMethod()
-----------------------------------
singleton.Singleton1@10bedb4
singleton.Singleton1@10bedb4
1.2 破壞單例的幾種情況
- 反射破壞單例
- 反序列化破壞單例
- Unsafe破壞單例
演示:
/**
* @author xppll
* @date 2021/12/24 21:28
*/
public class TestSingleton {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
//觸發Singleton1類的初始化,會為類的靜態變數賦予正確的初始值,單例物件就會被創建!
Singleton1.otherMethod();
System.out.println("-----------------------------------");
System.out.println(Singleton1.getINSTANCE());
System.out.println(Singleton1.getINSTANCE());
//反射破壞單例
reflection(Singleton1.class);
//反序列化破壞單例
serializable(Singleton1.getINSTANCE());
//Unsafe破壞單例
unsafe(Singleton1.class);
}
//反射破壞單例
private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//得到無參
Constructor<?> constructor = clazz.getDeclaredConstructor();
//將此物件的 accessible 標志設定為指示的布林值,即設定其可訪問性
constructor.setAccessible(true);
//創建實體
System.out.println("反射創建實體:" + constructor.newInstance());
}
//反序列化破壞單例
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
//序列化
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
//反序列化
System.out.println("反序列化創建示例:" + ois.readObject());
}
//Unsafe破壞單例
private static void unsafe(Class<?> clazz) throws InstantiationException {
Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
System.out.println("Unsafe 創建實體:" + o);
}
}
結果:

可以看出三種方式都會破壞單例!
1.3 預防單例的破壞
預防反射破壞單例
在構造方法中加個判斷即可:
//構造私有
private Singleton1() {
//防止反射破壞單例
if(INSTANCE!=null){
throw new RuntimeException("單例物件不能重復創建");
}
System.out.println("private Singleton1()");
}
預防反序列化破壞單例
在Singleton1()中重寫readResolve方法:
//重寫這個方法,如果序列化了,就會回傳這個,不會回傳反序列化的物件
public Object readResolve(){
return INSTANCE;
}
Unsafe破壞單例無法預防
2.列舉餓漢式
2.1 列舉單例實作
列舉實作單例:
/**
* 列舉實作單例
*
* @author xppll
* @date 2021/12/24 22:23
*/
public enum Singleton2 {
INSTANCE;
//列舉的構造方法默認是private的,可以不寫
Singleton2() {
System.out.println("private Singleton2()");
}
//重寫toString方法
@Override
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//獲得實體方法(這個可以不要,列舉變數都是public的)
public static Singleton2 getInstance() {
return INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
測驗:
/**
* @author xppll
* @date 2021/12/24 21:28
*/
public class TestSingleton {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
//觸發Singleton2類的初始化,會為類的靜態變數賦予正確的初始值,單例物件就會被創建!
Singleton2.otherMethod();
System.out.println("-----------------------------------");
System.out.println(Singleton2.getInstance());
System.out.println(Singleton2.getInstance());
}
}
//輸出:
private Singleton2()
otherMethod()
-----------------------------------
singleton.Singleton2@2de80c
singleton.Singleton2@2de80c
可以看出當呼叫otherMethod()時,就會觸發類的加載,列舉物件就會創建,所以列舉實作單例是餓漢式的
2.2 破壞單例
列舉類實作單例的好處:
- 反序列化無法破壞列舉單例
- 反射無法破壞列舉單例
栗子:
需要先修改反射破壞代碼,列舉需要有參構造
public class TestSingleton {
public static void main(String[] args) throws Exception {
Singleton5.otherMethod();
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(Singleton5.getInstance());
System.out.println(Singleton5.getInstance());
//反序列化破壞單例
serializable(Singleton2.getInstance());
//Unsafe破壞單例
unsafe(Singleton2.class);
//反射破壞單例
reflection(Singleton2.class);
}
private static void unsafe(Class<?> clazz) throws InstantiationException {
Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
System.out.println("Unsafe 創建實體:" + o);
}
private static void serializable(Object instance) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
System.out.println("反序列化創建實體:" + ois.readObject());
}
private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
System.out.println("反射創建實體:" + constructor.newInstance());
}
}
結果:

可以看出
- 反射是無法創建列舉物件!無法破壞列舉單例
- 反序列化也不會破壞列舉單例!
- Unsafe依然會破壞!
3.懶漢式
實作代碼如下:
/**
* 懶漢式
*
* @author xppll
* @date 2021/12/25 08:34
*/
public class Singleton3 implements Serializable {
//構造私有
private Singleton3() {
System.out.println("private Singleton3()");
}
//唯一實體
private static Singleton3 INSTANCE = null;
public static Singleton3 getInstance() {
//第一次呼叫的時候才創建
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
測驗:
/**
* @author xppll
* @date 2021/12/24 21:28
*/
public class TestSingleton {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton3.otherMethod();
System.out.println("-----------------------------------");
System.out.println(Singleton3.getInstance());
System.out.println(Singleton3.getInstance());
}
}
結果:

可以看出只有在第一次呼叫getInstance()時才會創建唯一的單例物件,因此是懶漢式的,
但是這種方式在多執行緒環境下是會有問題的,可能多個執行緒會同時執行INSTANCE = new Singleton3();,因此這里需要在getInstance()方法上加上synchronized關鍵字保證多執行緒下的正確性:
public static synchronized Singleton3 getInstance() {
//第一次呼叫的時候才創建
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
但是這種方法是有問題的,第一次創建完物件后,以后的操作是不需要在加鎖的,所以這種方式會影響性能!
我們的目標應該是第一次創建單例的時候給予保護,后續操作則不需要加鎖保護!
4.雙檢鎖懶漢式
針對上面的問題,這里給出第四種方法雙檢鎖懶漢式進行優化:
/**
* 雙檢鎖懶漢式
*
* @author xppll
* @date 2021/12/25 08:53
*/
public class Singleton4 {
//構造私有
private Singleton4() {
System.out.println("private Singleton4()");
}
//唯一實體
//這里volatile的作用是保證共享變數有序性!
private static volatile Singleton4 INSTANCE = null;
//雙檢鎖優化
public static synchronized Singleton4 getInstance() {
//實體沒創建,才會進入內部的 synchronized 代碼塊,提高性能,防止每次都加鎖
if (INSTANCE == null) {
//可能第一個執行緒在synchronized 代碼塊還沒創建完物件時,第二個執行緒已經到了這一步,所以里面還需要加上判斷
synchronized (Singleton4.class) {
//也許有其他執行緒已經創建實體,所以再判斷一次
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
關于這里的雙檢鎖判斷和volatile的使用可以看看我的這篇文章4-3節 :double-checked locking 問題
5.內部類懶漢式
內部類懶漢式單例實作:
/**
* 內部類懶漢式
*
* @author xppll
* @date 2021/12/25 09:24
*/
public class Singleton5 {
//構造私有
private Singleton5() {
System.out.println("private Singleton5()");
}
//靜態內部類實作懶漢式單例,靜態變數的創建會放在靜態代碼塊里執行,jvm會保證其執行緒安全
//只有第一次用到內部類時,才會初始化創建單例
private static class Holder {
static Singleton5 INSTANCE = new Singleton5();
}
//獲得實體方法
public static Singleton5 getInstance() {
return Holder.INSTANCE;
}
//其他方法
public static void otherMethod() {
System.out.println("otherMethod()");
}
}
測驗:
/**
* @author xppll
* @date 2021/12/24 21:28
*/
public class TestSingleton {
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
Singleton5.otherMethod();
System.out.println("-----------------------------------");
System.out.println(Singleton5.getInstance());
System.out.println(Singleton5.getInstance());
}
}
結果:

可以看出內部類實作單例也是懶漢式的!
6.JDK中單例的體現
Runtime 體現了餓漢式單例
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-N3tL76fL-1640434813190)(單例模式詳解.assets/image-20211225094117525.png)]](https://img.uj5u.com/2021/12/26/291864260821105.png)
System類下的Console 體現了雙檢鎖懶漢式單例
![[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-B9vf09sf-1640434813191)(單例模式詳解.assets/image-20211225094222659.png)]](https://img.uj5u.com/2021/12/26/291864260821106.png)
Collections 中的 EmptyNavigableSet內部類懶漢式單例

Collections 中的ReverseComparator.REVERSE_ORDER 內部類懶漢式單例

Comparators.NaturalOrderComparator.INSTANCE 列舉餓漢式單例


轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/393129.html
標籤:java
上一篇:MyBatis筆記
下一篇:波吉學設計模式——玩轉單例模式
