一、什麼是單例模式
單例模式確保一個類只有一個實例,並提供一個全局訪問點,實現單例模式的方法是私有化構造函數,通過getInstance()方法實例化對象,並返回這個實例。在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。
二、單例模式的特點
- 單例類只能有一個實例。
- 單例類必須自己創建自己唯一的實例。
- 單例類必須給所有其它對象提供這一實例。
三、單例模式的優缺點
優點:
- 單例類只有一個實例
- 共享資源,全局使用
- 節省創建時間,提高性能
缺點:可能存在線程不安全問題。
四、單例模式的七種寫法
分別是「餓漢」、「懶漢(非線程安全)」、「懶漢(線程安全)」、「雙重校驗鎖」、「靜態內部類」、「枚舉」和「容器類管理]
1、餓漢式
public class SingletonV1 {
/**
* 餓漢式:
* 優點:先天線程安全。當類被加載的時候,就會創建對象。
* 缺點:1、如果項目中使用過多的餓漢式會發生問題,項目在啓動的時候會變的很慢。
* 2、singletonV1對象是static的,所以會存放在方法區裏面,佔用內存大。
* 3、如果用戶不使用該對象,也會被提前創建好,比較浪費資源。
*/
private static SingletonV1 singletonV1 = new SingletonV1();
/**
* 將構造函數私有化,禁止初始化
*/
private SingletonV1(){}
public static SingletonV1 getInstance(){
return singletonV1;
}
public static void main(String[] args) {
SingletonV1 v1 = SingletonV1.getInstance();
SingletonV1 v2 = SingletonV1.getInstance();
//結果爲true
System.out.println(v1 == v2);
}
}
java.lang.Runtime源碼分析:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
2、懶漢式(線程不安全)
public class SingletonV2 {
//懶漢式,線程不安全
private static SingletonV2 singletonV2;
private SingletonV2(){}
public static SingletonV2 getInstance(){
if (singletonV2 == null) {
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
singletonV2 = new SingletonV2();
}
return singletonV2;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
SingletonV2 instance2 = SingletonV2.getInstance();
System.out.println(Thread.currentThread().getName()+" - "+instance2);
}
}).start();
}
}
}
結果:線程不安全
Thread-2 - com.xwhy.singleton.v2.SingletonV2@1dda492b
Thread-13 - com.xwhy.singleton.v2.SingletonV2@46360c95
Thread-5 - com.xwhy.singleton.v2.SingletonV2@4e1de1c0
Thread-0 - com.xwhy.singleton.v2.SingletonV2@64403094
Thread-11 - com.xwhy.singleton.v2.SingletonV2@4a86c08f
Thread-12 - com.xwhy.singleton.v2.SingletonV2@5824138f
Thread-24 - com.xwhy.singleton.v2.SingletonV2@66b01c61
Thread-31 - com.xwhy.singleton.v2.SingletonV2@17733f02
Thread-44 - com.xwhy.singleton.v2.SingletonV2@579ade96
Thread-43 - com.xwhy.singleton.v2.SingletonV2@60fe2295
Thread-6 - com.xwhy.singleton.v2.SingletonV2@1b4b1c5b
Thread-49 - com.xwhy.singleton.v2.SingletonV2@1f904935
Thread-3 - com.xwhy.singleton.v2.SingletonV2@4e1de1c0
...
3、懶漢式(線程安全)
public class SingletonV3 {
//懶漢式,線程不安全
private static SingletonV3 singletonV3;
private SingletonV3(){}
/**
* 效率低
* @return
*/
public static synchronized SingletonV3 getInstance(){
try {
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
}
if (singletonV3 == null) {
System.out.println("創建實例singletonV3");
singletonV3 = new SingletonV3();
}
System.out.println("獲取實例singletonV3");
return singletonV3;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
public void run() {
SingletonV3 singletonV3 = SingletonV3.getInstance();
System.out.println(Thread.currentThread().getName()+" - "+singletonV3);
}
}).start();
}
}
}
結果:
創建實例singletonV3
獲取實例singletonV3
Thread-0 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-99 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-98 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
Thread-97 - com.xwhy.singleton.v3.SingletonV3@470bc213
獲取實例singletonV3
...
4、雙重檢驗鎖(DCL)
/*
* 解決懶漢式獲取對象的效率問題
*/
public class SingletonV4 {
/**
* volatile 禁止指令重排序,增加可見性
*/
private volatile static SingletonV4 singletonV4;
private SingletonV4(){}
public static SingletonV4 getInstance(){
//第一次判斷如果沒有創建對象就開始加鎖
if(singletonV4 == null){
//當用戶搶到鎖,判斷初始化
synchronized (SingletonV4.class){
if(singletonV4 == null){
singletonV4 = new SingletonV4();
}
}
}
return singletonV4;
}
/**
* 測試單例
*/
public static void main(String[] args) {
for (int i=0;i<100;i++){
new Thread(new Runnable() {
public void run() {
SingletonV4 instance4 = SingletonV4.getInstance();
System.out.println(Thread.currentThread().getName()+","+instance4);
}
}).start();
}
}
}
結果:線程安全
Thread-1,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-4,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-5,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-2,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-3,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-7,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-0,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-8,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-6,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-9,com.xwhy.singleton.v4.SingletonV4@64403094
Thread-10,com.xwhy.singleton.v4.SingletonV4@64403094
....
5、靜態內部類
public class SingletonV5 {
private SingletonV5(){
System.out.println("構造函數執行...");
}
/**
* 靜態內部類特徵:繼承懶漢式和餓漢式優點,同時解決雙重檢驗鎖的第一次加載慢的問題
* 讀和寫都不需要同步所以效率非常高
* 內部類被調用的時候纔會去初始化singletonV5
*/
private static class SingletonHolder{
private static final SingletonV5 singletonV5 = new SingletonV5();
}
public static SingletonV5 getInstance(){
return SingletonHolder.singletonV5;
}
public static void main(String[] args) {
System.out.println("項目啓動成功。。。");
SingletonV5 instance1 = SingletonV5.getInstance();
SingletonV5 instance2 = SingletonV5.getInstance();
System.out.println(instance1==instance2);
}
}
結果:
項目啓動成功。。。
對象初始化...
true
6、枚舉形式
public enum SingletonEnum {
INSTANCE;
// 枚舉能夠絕對有效的防止實例化多次,和防止反射和序列化破壞
public void add() {
System.out.println("add方法...");
}
/**
* 測試單例
*/
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
System.out.println(instance1==instance2);
Constructor<SingletonEnum> declaredConstructor = SingletonEnum.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonEnum v6 = declaredConstructor.newInstance();
System.out.println(v6==instance1);
}
}
輸出結果:
true
Exception in thread "main" java.lang.NoSuchMethodException: com.xwhy.singleton.v6.SingletonEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.xwhy.singleton.v6.SingletonEnum.main(SingletonEnum.java:24)
枚舉反射破解不了單例
7、使用容器管理
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
{
return objMap.get(key);
}
}
}
這種使用SingletonManager 將多種單例類統一管理,在使用時根據key獲取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度。
五、如何防止破壞單例
雖然單例通過私有構造函數,可以實現防止程序員初始化對象,但是還可以通過反射和序列化技術破壞單例。
1、使用反射技術破壞單例
// 1. 使用懶漢式創建對象
SingletonV3 instance1 = SingletonV3.getInstance();
// 2. 使用Java反射技術初始化對象 執行無參構造函數
Constructor<SingletonV3> declaredConstructor = SingletonV3.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
SingletonV3 instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
如何防止被反射破壞
私有構造函數
private SingletonV3() throws Exception {
synchronized (SingletonV3.class) {
if (singletonV3 != null) {
throw new Exception("該對象已經初始化..");
}
System.out.println("執行SingletonV3無參構造函數...");
}
}
2、使用序列化技術破壞單例
Singleton instance = Singleton.getInstance();
FileOutputStream fos = new FileOutputStream("E:\\code\\Singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("E:\\code\\Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Singleton singleton2 = (Singleton) ois.readObject();
System.out.println(singleton2==instance)
//返回序列化獲取對象 ,保證爲單例
public Object readResolve() {
return singletonV3;
}
六、枚舉單例爲什麼不能夠反射初始化
枚舉底層原理
1、首先如果使用java反射機制破壞單例,報錯
通過該錯誤說明,枚舉類中沒有無參的構造函數
2、使用java反編譯技術,查看枚舉類
從該圖可以看出,枚舉底層其實是一個類
3、枚舉類底層原理分析
使用靜態代碼塊的方式,當靜態代碼塊執行當時候初始化對象,從而可以讓開發者可以通過EnumSingleton.INSTANCE
使用。
4、在該反編譯源碼中,定義了一個類繼承了Enum 該類是中沒有無參構造函數,所以反射機制調用無參構造函數是無法初始化的。
5、在該類中有一個只有一個有參構造函數
調用父類構造構造函數
Name 爲定義調用對象名稱,定義調用對象序號ordinal
6、使用注入有參構造函數是否可以破壞枚舉呢?也不行的。
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingleton v3 = declaredConstructor.newInstance();
System.out.println(v3 == instance1);
爲什麼報這個錯誤呢?主要原因是 java的反射初始化對象中,只要對象是枚舉類型是不會初始化的。