1. 基本概念
java 進程 內(nèi)存中只有一個 對象實例。
- 實現(xiàn)的基本原則:
- 構(gòu)造器私有化,不允許外部創(chuàng)建對象。
- 提供public static 的訪問點,返回創(chuàng)建的對象。
- 應(yīng)用場景:
- spring ioc 容器默認單例。
- 全局配置對象,全局一份
- 框架封裝,結(jié)合其他設(shè)計模式一起使用。
2. 實現(xiàn)方式
- 餓漢式
- 懶漢式
雙重檢查鎖
靜態(tài)內(nèi)部類
- 3.ThreadLocal
- 枚舉
- CSA 原子類
- 注冊式單例
3. 破壞單例的方式
- 暴力反射
- 序列化和反序列化
4.代碼實現(xiàn)
上面我們簡單的總結(jié)了一下有關(guān)單例模式的相關(guān)知識點,接下來我們就來用代碼實現(xiàn)一下常見單例的幾種寫法。以及如何保證單例的線程安全。
4.1 餓漢式
public class HungarySingleton {
//類加載時進行初始化
private static final HungarySingleton instance = new HungarySingleton();
// 構(gòu)造器初始化
private HungarySingleton() {}
// 全局訪問點
public static HungarySingleton getInstance() {
return instance;
}
}
優(yōu)點:餓漢式在類加載時就初始對象,并且只初始化一次,所以是線程安全的。
缺點:這種寫法是強引用,在JVM 里面永遠不會被回收。同時在jvm 啟動時也會消耗一定的資源,不管是否使用,都已經(jīng)創(chuàng)建了,存在資源的浪費,如果在jvm 里面餓漢式單例太多了,就很浪費資源了,并且被創(chuàng)建的對象也無法被垃圾回收。后面我們會講懶加載,這里我們首先來測試一下單例破壞的反射和序列化。
- 反射破壞:
/**
* 反射破壞
* @throws Exception
*/
public static void test2() throws Exception {
//得到默認構(gòu)造器
Constructor<HungarySingleton> declaredConstructor = HungarySingleton.class.getDeclaredConstructor();
//強制訪問
declaredConstructor.setAccessible(true);
// 創(chuàng)建對象
HungarySingleton hungarySingleton = declaredConstructor.newInstance();
System.out.println(hungarySingleton);
HungarySingleton instance = HungarySingleton.getInstance();
System.out.println(instance);
}
- 測試結(jié)果
com.example.designpattern.singleton.HungarySingleton@7440e464
com.example.designpattern.singleton.HungarySingleton@49476842
Process finished with exit code 0
從上面的測試結(jié)果看出來,餓漢式創(chuàng)建的單例可以被發(fā)射破壞。為了解決這個問題我們可以 在構(gòu)造器那里做一下手腳:因為是反射調(diào)用構(gòu)造器,所以我們可以在構(gòu)造器中判斷一下,如果對象已經(jīng)存在了就拋出異常,防止再創(chuàng)建一次對象。
- 修改構(gòu)造器:
public class HungarySingleton {
//類加載時進行初始化
private static final HungarySingleton instance = new HungarySingleton();
// 構(gòu)造器初始化
private HungarySingleton() {
if (instance!=null) {//判斷對象是否已經(jīng)被創(chuàng)建
throw new RuntimeException("請不要重復(fù)創(chuàng)建對象");
}
}
// 全局訪問點
public static HungarySingleton getInstance() {
return instance;
}
}
- 測試結(jié)果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.example.designpattern.singleton.HungarySingleton.test2(HungarySingleton.java:62)
at com.example.designpattern.singleton.HungarySingleton.main(HungarySingleton.java:48)
Caused by: java.lang.RuntimeException: 請不要重復(fù)創(chuàng)建對象
at com.example.designpattern.singleton.HungarySingleton.<init>(HungarySingleton.java:22)
... 6 more
Process finished with exit code 1
從上面的測試結(jié)果我們可以看出,反射是可以破壞單例的,當(dāng)然針對餓漢式單例的反射破壞我們也可以有一些措施。接下來我們來看看 序列化和反序列化是如何破壞單例的。
- 序列化和反序列化破壞單例
/**
* 序列化和反序列化破壞單例
* */
public static void test3() throws IOException, ClassNotFoundException {
//將對象寫到磁盤
HungarySingleton hungarySingleton = HungarySingleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("hungarySingleton.obj"));
outputStream.writeObject(hungarySingleton);
System.out.println(hungarySingleton);
//然后再將對象從磁盤讀取出來
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("hungarySingleton.obj"));
HungarySingleton object = (HungarySingleton)inputStream.readObject();
System.out.println(object);
}
- 測試結(jié)果
餓漢式=com.example.designpattern.singleton.HungarySingleton@5451c3a8
序列化和反序列化=com.example.designpattern.singleton.HungarySingleton@3d494fbf
上述測試結(jié)果表明 序列化和反序列化也能夠?qū)卫斐善茐模梢蚤喿x源碼找到原因。注意這里我是實現(xiàn)了 Serializable 接口的。
- 應(yīng)對策略
- 不要實現(xiàn) Serializable 接口。序列化和反序列化要求必須實現(xiàn) Serializable 接口,所以為了 防止 序列化和反序列化對單例的破壞,可以不要實現(xiàn) Serializable 接口。
- 如果業(yè)務(wù)要求必須要實現(xiàn)Serializable 接口,那么就只有下面這一種方式可以應(yīng)對:重寫 readResolve() 方法。
/**
* 防止序列化和反序列化對單例的破壞,返回單例對象
* @return
*/
public Object readResolve() {
return instance;
}
上面這個返回會在 HungarySingleton object = (HungarySingleton)inputStream.readObject(); 這句話執(zhí)行的時候 回調(diào),直接就返回你自己 返回的對象,所以可以 應(yīng)對 對單例的破壞。具體的可以看看 jdk 的源碼,不能夠找到答案。上面我們實現(xiàn)了餓漢式單例,并且分析了餓漢式單例的優(yōu)缺點,以及反射和序列化,反序列化對單例的破壞。以及相應(yīng)的應(yīng)對策略。下面的內(nèi)容我們就只針對 單例的一些實現(xiàn)展開談?wù)?,對單例的破壞就不做分析了,可以自己去測試。
4.2 懶加載
- 雙重檢查鎖
在double check 之前我們先來看看 為什么會出現(xiàn) double check 這種寫法。
最簡單的懶加載:
public class SimpleSingleton {
//1.靜態(tài)成員
private static SimpleSingleton instance;
//2. 構(gòu)函數(shù)私有化
private SimpleSingleton() {}
//3. 提供全局訪問方法
public static SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
- 單線程測試:
//單線程測試
public static void test() {
SimpleSingleton instance = SimpleSingleton.getInstance();
SimpleSingleton instance2 = SimpleSingleton.getInstance();
SimpleSingleton instance3 = SimpleSingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance3);
}
com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464
從上面的單線程測試中沒有發(fā)現(xiàn)問題,接下來進行多線程測試,這里我們就使用簡單的多線程測試,也可以并發(fā)測試。
- 多線程測試
//多線程測試
public static void test2() {
for (int i=0 ;i<3; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleSingleton instance = SimpleSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
com.example.designpattern.singleton.SimpleSingleton@21279f82
com.example.designpattern.singleton.SimpleSingleton@25f209f5
com.example.designpattern.singleton.SimpleSingleton@6e154e44
這里我們看到直接就產(chǎn)生了3個不同的對象,顯然違背單例的設(shè)計思想。針對線程安全問題,我們可以使用 鎖來解決這個問題。于是就有了下面幾種線程安全的寫法。
- 靜態(tài)同步方法和同步代碼塊
// 靜態(tài)方法上面 加 synchronized 關(guān)鍵字
public static synchronized SimpleSingleton getInstance() {
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
// 同步代碼塊
public static SimpleSingleton getInstance() {
synchronized(SimpleSingleton.class) {//這里的鎖 一般這樣寫,但也可以是 靜態(tài)對象 作為鎖
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
上面的 兩種寫法 效果完全一樣,效率也一樣,鎖的范圍也一樣,都是 類鎖。至于為什么是 類級別的鎖,不是 對象級別的,有下面幾個原因:
1.靜態(tài)方法本身是可以使用類直接調(diào)用,也就在類級別 ,在靜態(tài)方法上面加鎖的化 自然也就是類級別的鎖了;
2 . 靜態(tài)方法里面的 同步代碼塊為啥 是 類級別的鎖。注意這里因為我用的 SimpleSingleton.class 作為鎖,所以說上面的兩種寫法是等效的。一般使用 SimpleSingleton.class作為鎖是 避免創(chuàng)建 其他鎖對象,這里是不能使用 this 作為鎖的,這也是 因為有 static 關(guān)鍵字的原因。
3 . 在這里能不能使用 對象鎖呢?答案是可以的,但是必須是 static 對象 如:
// 創(chuàng)建一個對象作為鎖
final static Object object = new Object();
public static SimpleSingleton getInstance() {
synchronized(object) {// 使用對象鎖
if (instance == null) {
instance = new SimpleSingleton();
}
return instance;
}
}
//顯然這樣的寫法沒有 SimpleSingleton.class 作為鎖簡單,
//因為你自己又單獨 創(chuàng)建了一個 Object 對象,而且還是 餓漢式創(chuàng)建的。
分析完了上面的 鎖的問題,我們再來分析一下 這種寫法的優(yōu)缺點,是否值得我們平時的項目中使用:
1 .優(yōu)點:synchronized 關(guān)鍵字保證了線程安全,同時也是懶加載的。
2 .缺點:從上面的代碼中我們可以看到 我們的鎖都是全局鎖,也就是說 每一個線程來訪問我們的方法的時候 被要先去 獲得鎖,方法執(zhí)行完了以后再去釋放鎖。我們知道,單例對象只在第一次訪問的時候 創(chuàng)建就ok 了 ,也就是 下面這個邏輯 只在 第一次 訪問該方法的 時候 instance == null ,然后創(chuàng)建 對象。如果在 線程安全的情況下 后續(xù)的線程 的 instance 都是不為空的,就不會去創(chuàng)建對象了,也就保證了線程安全了,那么我們對整個方法 都加上鎖 就很低效了。
if (instance == null) {// 第一次訪問的時候 滿足
instance = new SimpleSingleton();
}
上面我們分析出 靜態(tài)同步方法和同步代碼塊 雖然能夠保證線程安全,但是也帶來可一些性能問題,那么我們就 優(yōu)化一下性能就ok 了。既然instance 只有在 第一次 訪問的 時候 是 null 那么 我們就在 if 里面來 加一個鎖 也就是在 第一次創(chuàng)建的時候 保證 線程安全就ok了,后面的線程 都不會 進入 if ,所以就有了下面的優(yōu)化方案:
public static SimpleSingleton getInstance() {
if (instance == null) {
synchronized (SimpleSingleton.class) {
instance = new SimpleSingleton();
}
}
return instance;
}
- 測試:
public static void test2() {
for (int i=0 ;i<3; i++) {
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
SimpleSingleton instance = SimpleSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
com.example.designpattern.singleton.SimpleSingleton@6e154e44
com.example.designpattern.singleton.SimpleSingleton@2fd04fd1
com.example.designpattern.singleton.SimpleSingleton@21279f82
測試結(jié)果我們發(fā)現(xiàn),我擦,怎么會呢?不是加了鎖了嗎??我們仔細來分析一下為啥會是這樣的:
public static SimpleSingleton getInstance() {
if (instance == null) {// 1. 當(dāng)?shù)谝淮?到這里的就只有一個 線程 1 ,那么這個是線程安全的;2 . 當(dāng)線程 1 和線程 2 同時到 這里了,不管是線程1 和線程 2 誰獲得了鎖 都會是 執(zhí)行 創(chuàng)建對象的 語句,也就有了多個對象。
synchronized (SimpleSingleton.class) {
instance = new SimpleSingleton();
}
}
return instance;
}
針對上面的問題 我們自然就有了 下面的寫法,也就是我們在 同步代碼塊里面再判斷一次,就可以保證線程安全了,也就有了 雙重檢查鎖的寫法:
public static SimpleSingleton getInstance() {
if (instance == null) {// 當(dāng)線程1 和線程2 都執(zhí)行到了這里,假設(shè)線程 1 獲取了鎖
synchronized (SimpleSingleton.class) {
if (instance == null) {
instance = new SimpleSingleton();// 線程1 創(chuàng)建了 對象,執(zhí)行完畢 退出 同步代碼塊,釋放鎖,這時候 線程 2 獲取了鎖,然后 讀取了 instance 的值 發(fā)現(xiàn) 不為空 ,第二個 if 條件就不滿足了,不會執(zhí)行 對象創(chuàng)建的語句。
}
}
}
return instance;
}
到這里我們就 把為啥會出現(xiàn) double check 的過程分析了一下,但是 上面 的寫法都還不是 線程安全的,因為 instance = new SimpleSingleton(); 在jvm 創(chuàng)建對象的指令 中 不是原子的,也就說 jvm 創(chuàng)建對象 至少有 下面幾條指令:1. 申請一塊內(nèi)存空間; 2. 創(chuàng)建一個對象;3. 將地址值賦值個 變量;在并發(fā)量高的情況下,可能會發(fā)生可見性問題和指令重排序問題,
為了解決這個問題 我們可以 使用 volatile 來 修飾 instance。下面我們來看看 完整的 double check 是怎么寫的。
public class DoubleCheckSingleton {
// volatile 保證可見性,防止指令重排序
private static volatile DoubleCheckSingleton singleton;
public static DoubleCheckSingleton doubleCheckSingleton() {
if (singleton == null) {
synchronized (DoubleCheckSingleton.class) {
if (singleton == null ) {//為了防止 兩個線程都進了 第一個if導(dǎo)致的線程安全問題,所以可以再加一次判斷
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}
上面我們分析了double check ,一切看上去都很完美,但是就是有一點,使用了線程同步機制,來保證線程的安全性,那么有沒有一種不使用線程同步機制 也可以 實現(xiàn)線程安全和懶加載呢?答案是肯定的,那就是靜態(tài) 內(nèi)部類。另外,在這里我們沒有分析 反射 ,序列化和反序列化對單例的破壞。答案是這兩種都是可以破壞 double check 的單例,可以自己測試一下。接下來我們來分析一下 靜態(tài)內(nèi)部類的單例。
4.2靜態(tài)內(nèi)部類
public class InnerStaticSingleton {
private InnerStaticSingleton() {}
//但外部調(diào)用 SingletonHolder.innerStaticSingleton 時才會加載這里的靜態(tài)內(nèi)部類
private static class SingletonHolder{
private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton();
}
public static InnerStaticSingleton getInstance() {
return SingletonHolder.innerStaticSingleton;
}
}
上面是靜態(tài)內(nèi)部類的實現(xiàn)方式,這里我解釋一下,為什么是懶加載的。在外部類加載到j(luò)vm 時,靜態(tài)內(nèi)部類是不會被加載的,也就不會執(zhí)行 private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton(); 只有當(dāng) 外部類的 static 方法被調(diào)用時,才會 加載 內(nèi)部類,并實例化對象。靜態(tài)在整個 jvm 運行周期中都只加載一次,所以是可以保證單例的,根據(jù)前面的分析,只有在調(diào)用時才會去初始化對象,所以是懶加載的。至于線程安全,也是利用jvm 內(nèi)部機制保證的,到底是如何保證的,由于筆者水平有限,暫無法解釋,希望大家留言談?wù)摗T摲绞酵ǔ1徽J為是最優(yōu)的 單例實現(xiàn)方式,但是也有一個缺點,就是參數(shù)傳遞的問題。所以到底要使用哪一種實現(xiàn)方式,是取決于 實際應(yīng)用場景的。雖然該方式 優(yōu)雅,但是同樣可以被反射和序列化破壞。那么到底有沒有一種單例是能夠防止反射和序列化的破壞,答案是肯定的,那就是枚舉式單例,終極殺招。
4.3枚舉式單例
public enum EnumSingleton {
INSTANCE;
}
這就是枚舉式單例,是不是非常簡單。下面我們來測試一下反射和序列化得破壞結(jié)果,看看是否能達到我們的預(yù)期。
- 反射破壞
public static void test() throws Exception{
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance();
System.out.println(enumSingleton);
}
- 測試結(jié)果
Exception in thread "main" java.lang.NoSuchMethodException: com.example.designpattern.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:23)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)
上面是反射的測試結(jié)果,直接給我們異常了,說沒有默認構(gòu)造器,后面我們會 分析一下 底層的原來,看看枚舉單例到底是怎么回事。這里我們再來測試一下序列化。
- 序列化測試
public static void test2() throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println(instance);
outputStream.writeObject(instance);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
System.out.println(enumSingleton);
}
- 測試結(jié)果:
INSTANCE
INSTANCE
結(jié)果返回了同一個對象,說明jdk 也為我們屏蔽了序列化對單例的影響。到這來是不是覺得很牛叉,枚舉都幫我們做了,首先是線程安全的,其次反射和序列化也不能破壞它,但是是不是懶加載的呢?肯定不是,因為枚舉也是在jvm 加載的時候就會初始化的。在 Effective java 那本書里面,作者就推薦使用 枚舉式單例。當(dāng)然到底使用哪一種,我們還是要根據(jù)業(yè)務(wù)場景來選擇。好了,既然枚舉這么牛掰,我們能不能看看jvm 在加載 枚舉的時候,到時是怎么做到的?接下來我們就來看看 枚舉的神秘面紗。首先,從代碼層面看不出啥東西,那么,我們就要想辦法看看他編譯后的樣子。
反編輯工具 jad
- 反編譯 EnumSingleton.class
jad EnumSingleton.class 。會生成一個 EnumSingleton.jad 的文件。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingleton.java
package com.example.designpattern.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
public final class EnumSingleton extends Enum
{
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
// 構(gòu)造器,私有化,沒有無參構(gòu)造器,所以我們在測試的時候 會拋出異常,說沒有無參構(gòu)造器。
private EnumSingleton(String s, int i)
{
super(s, i);
}
// 靜態(tài)代碼塊初始化對象,餓漢式寫法,是線程安全的。
static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/example/designpattern/singleton/EnumSingleton, name);
}
}
上面是 EnumSingleton.class 反編譯的結(jié)果。我們看到 EnumSingleton 枚舉 繼承的 Enum 對象,該對象是 jdk 自帶的java.lang下面的抽象類
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
protected Enum(String name, int ordinal) {//只有這一個構(gòu)造函數(shù)
this.name = name;
this.ordinal = ordinal;
}
}
上面解釋了我們在反射 調(diào)用無參構(gòu)造函數(shù)的時候,為啥會有異常拋出,那是因為枚舉本身就沒有無參構(gòu)造函數(shù)。好了,到這里可能你又發(fā)現(xiàn)了,雖然沒有無參構(gòu)造函數(shù),但是有 兩個帶參數(shù)的構(gòu)造函數(shù),我們能不能調(diào)用呢?我們來測試一下就知道了。
public static void test() throws Exception{
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class); //得到帶有參數(shù)的構(gòu)造函數(shù)
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance("測試",007);// 調(diào)用,創(chuàng)建對象
System.out.println(enumSingleton);
}
- 測試結(jié)果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:25)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)
上面結(jié)果說,不能通過反射來創(chuàng)建 枚舉對象。他說在 java.lang.reflect.Constructor.newInstance(Constructor.java:417) 417行 拋出的異常。我們就去看一下 :
- Constructor
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)// 這里的意思是 枚舉的話 就 拋出異常。
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
好了,上面我們就解釋了,為啥枚舉能夠防止反射破壞單例,原來是jdk 幫我們做了這個事情了。我們還有一個序列化破壞沒有找到原因。接下來我們就來看看序列化的原因,由于篇幅太長,可能已經(jīng)忘記了 序列化測試代碼,我們再來貼一下:
public static void test2() throws Exception{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println(instance);
outputStream.writeObject(instance);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
System.out.println(enumSingleton);
}
上面代碼就兩個意思,1 . 把對象寫到磁盤;2 . 從磁盤讀出來對象。既然讀的時候得到的是一個對象,那么我們就直觀 的先從讀 開始,看看能不能找到答案,如果不能,我們再去 分析寫。
EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
看看 readObject()里面
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {// 這里是false
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false); //那么 對象就是從這里出來的,我們進去看看
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
----- enableOverride 我們使用的這個構(gòu)造器
public ObjectInputStream(InputStream in) throws IOException {
verifySubclass();
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
serialFilter = ObjectInputFilter.Config.getSerialFilter();
enableOverride = false; // false
readStreamHeader();
bin.setBlockDataMode(true);
}
------ Object obj = readObject0(false);
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM: // 前面的 啥邏輯 我們也看不太懂,但是這里可以看到是和枚舉相關(guān)的 ,那么我們 去看看 readEnum(unshared)
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
------ readEnum(unshared)
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);// 這里就是得到對象的 描述 ObjectStreamClass 對象,
if (!desc.isEnum()) {// 判斷是否是枚舉
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name); // 得到 Enum 對象,我們知道 枚舉 是繼承 Enum 的,這里也是 返回的 Enum 。通過 枚舉 class 對象 和 name 就得到 了一個唯一的對象,這個name 就是 我們通常自己定義的 枚舉的對象的name。我們可以繼續(xù) 下去,看看 Enum.valueOf((Class)cl, name); 里面是啥
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
----- name 枚舉對象的name
System.out.println(EnumSingleton.INSTANCE.name()); // INSTANCE
----- Enum.valueOf((Class)cl, name);
Enum :
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name); // 是從這里取取來的。
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
----------- T result = enumType.enumConstantDirectory().get(name);
Class<T> enumType:
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant); // 保存 枚舉對象
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
// 該map 不能被序列化
private volatile transient Map<String, T> enumConstantDirectory = null;
// 到這里我們也就看到了 原來 他還是一個 map 集合,map 里面就保存了枚舉對象,在被調(diào)用的時候,就判斷一下,如果是空,就存儲枚舉對象,然后返回,然后通過name 取獲取,每次都是獲取到的一個 對象,這個也叫做 注冊式單例,spring 就是典型的注冊式單例。
上面我們分析了 為啥反序列化得時候 ,得到的也是相同的枚舉對象,就是要因為 jvm 自己講枚舉對象存在了一個map 集合里面,然后每次都是去 map 里面取,對象也就只創(chuàng)建了一次,后面都是 讀出來的自然也就 是單例了,這也就 注冊式單例。到這里我們就分析完了,枚舉能夠防止反射和序列化破壞的原因了。
4.4 ThreadLocal 單例
上面枚舉單例里面提及到了注冊式單例,現(xiàn)在我們來看看另外一種注冊式單例--ThreadLocal 單例。
- ThreadLocalSingleton
public class ThreadLocalSingleton {
private ThreadLocalSingleton () {}
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {//初始化值
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance() {
return threadLocal.get();
}
}
ThreadLocal 本身的特點是變量和線程存在綁定的映射關(guān)系,我們先來看測試結(jié)果,就明白是啥意思了。為了方便理解ThreadLocal 的特點,我們使用線程池來測試。
- 線程池測試
public static void test() throws InterruptedException {
// 創(chuàng)建有5個線程的線程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0 ;i<10; i++) {// 開啟10 個線程,有線程池去處理。
executorService.submit(()->{
countDownLatch.countDown();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + "___" + instance);
});
}
countDownLatch.await();
}
- 測試結(jié)果
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
pool-1-thread-4___com.example.designpattern.singleton.ThreadLocalSingleton@447a380d
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
/**
* 注冊式單例
*/
public class RegisterSingleton {
private static Map<String,RegisterSingleton> map = new ConcurrentHashMap<>(1);
private RegisterSingleton() {}
private static volatile RegisterSingleton instance;
public static RegisterSingleton getInstance() {
instance = map.get("instance");
if (null == instance) {
synchronized (RegisterSingleton.class) {
instance = map.get("instance");
if (null == instance) {
instance = new RegisterSingleton();
map.put("instance",instance);
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i=0;i<100; i++) {
new Thread(()->{
RegisterSingleton instance = RegisterSingleton.getInstance();
System.out.println(instance);
}).start();
}
}
}
我們觀察相同線程 獲取到的對象是一樣的,這個就是ThreadLocal 本身的特點,也就說 同一個線程獲取到的對象始終是一個,對單個線程來說 這也就是單例了。但是對于不同的線程來說 是獲取到不同的對象。這個和ThreadLocal 本身的數(shù)據(jù)結(jié)構(gòu)有關(guān)系。我們可以去看一看jdk 的源碼,這里我們就不展開了,內(nèi)部是維護了一個 ThreadLocalMap 靜態(tài)內(nèi)部來存儲當(dāng)前線程的值,所以每個線程都有一個ThreadLocalMap 對象與之對應(yīng),獲取到值也只自己線程的。到此,我們可以總結(jié)出一個結(jié)論:注冊式單例就是 對象創(chuàng)建一次,然后存放到 Map 中,后面去Map 里面直接獲取就ok。
上面我們說了注冊式單例和 ThreadLocal 的單例,注冊式單例的實現(xiàn)方式有很多種,但是唯一不變的就是 底層的數(shù)據(jù)結(jié)構(gòu)一定是 Map 的,然后保證 在訪問Map 的時候 是線程安全的就行。最后介紹一種CAS實現(xiàn)的 單例。
- CAS 單例--原子引用類
public class CASSingleton {
private CASSingleton() {
}
private final static AtomicReference<CASSingleton> atomicReference = new AtomicReference<>();
public static CASSingleton getInstance() {
for (; ; ) {//自旋
CASSingleton casSingleton = atomicReference.get();
if (casSingleton != null) {
return casSingleton;
}
casSingleton = new CASSingleton();
// CSA 操作:如果當(dāng)前的值是 null,就更新 為 casSingleton
boolean compareAndSet = atomicReference.compareAndSet(null, casSingleton);
if (compareAndSet) {
return casSingleton;
}
}
}
- 測試:
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
CASSingleton instance = CASSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}).start();
}
CASSingleton instance = CASSingleton.getInstance();
CASSingleton instance2 = CASSingleton.getInstance();
CASSingleton instance3 = CASSingleton.getInstance();
CASSingleton instance4 = CASSingleton.getInstance();
System.out.println(instance);
System.out.println(instance2);
System.out.println(instance3);
System.out.println(instance4);
}
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-1:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-0:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-3:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-2:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-4:com.example.designpattern.singleton.CASSingleton@52cc8049
有關(guān)CAS 相關(guān)知識如果不熟悉的話,可以去學(xué)習(xí)一下并發(fā)編程相關(guān)的知識。
總結(jié)一下:
到此就介紹了單例的常見的實現(xiàn)方式:double check ,靜態(tài)內(nèi)部類,枚舉,餓漢式,ThreadLocal,CAS 單例,注冊式單例,當(dāng)然肯定還有其他的變種寫法,但是根本的原則不會改變-- JVM 中整個生命周期中只存在一個對象實例。同時,也分析了單例被破壞的情況,反射和序列化。當(dāng)然項目中不會故意去破壞,但是無意的破壞是可能的,比如反射破壞。好了,這就是筆者對單例模式的理解,如果不足之處,歡迎留言討論!