DesignPattern系列__10單例模式
單例模式介紹
單例模式,是為了確保在整個軟體體統中,某個類物件只有一個例項,並且該類通常會提供一個對外獲取該例項的public方法(靜態方法)。 比如日誌、資料庫連線池等物件,通常需要且只需要一個例項物件,這就會使用單例模式。
單例模式的7種模式
- 餓漢式
- 靜態常量
- 靜態程式碼塊
- 懶漢式
- 執行緒不安全
- 同步方法
- 同步程式碼塊
- 雙重檢查
- 靜態內部類
- 列舉
- 容器實現單例模式
- 執行緒池實現單例模式
下面依次來說明一下:
餓漢式(靜態常量)
通常,我們建立一個物件的方式就是new,但是,當我們考慮只建立一個例項的時候,就應該禁止外部來通過new的方式進行建立。同時,由於無法使用new,你應該考慮提供一個獲取單例物件的方式給別人。
思路
1.將構造器私有化(防止外部new,但是對反射還是有侷限) 2.類的內部建立物件 3.對外提供一個獲取例項靜態的public方法
程式碼實現:
public class Singleton1 {
public static void main(String[] args) {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
System.out.println(hungrySingleton == hungrySingleton1);
}
}
class HungrySingleton {
//1.私有化構造器
private HungrySingleton () {
}
// 2.類內部建立物件,因為步驟3是static的,
// 所以例項物件是static的
private final static HungrySingleton instance = new HungrySingleton();
//3.對外提供一個獲取物件的方法,
// 因為呼叫方式的目的就是為了獲取物件,
// 所以該方法應該是static的。
public static HungrySingleton getInstance() {
return instance;
}
}
複製程式碼
執行程式顯示,我們的確只建立了一個物件例項。
小結
優點:程式碼實現比較簡單,在類載入的時候就完成了例項化,同時,該方式能夠避免執行緒安全問題。 缺點:在類裝載的時候就完成例項化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個例項,則會造成記憶體的浪費。 這種方式基於classloder機制避免了多執行緒的同步問題,不過, instance在類裝載時就例項化,在單例模式中大多數都是呼叫getInstance方法, 但是導致類裝載的原因有很多種, 因此不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance就沒有達到lazy loading的效果。 總結:這種單例模式可以使用,但是可能造成記憶體的浪費。
餓漢式(靜態程式碼塊)
該方式和第一種區別不大,只是將建立例項放在了靜態程式碼塊中。 由於無法使用new,你應該考慮提供一個獲取單例物件的方式給別人。
思路
1.將構造器私有化(防止外部new,但是對反射還是有侷限) 2.類的內部建立物件(通過靜態程式碼塊) 3.對外提供一個獲取例項靜態的public方法
程式碼實現:
public class Singleton2 {
public static void main(String[] args) {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
System.out.println(hungrySingleton == hungrySingleton1);
}
}
class HungrySingleton {
//1.私有化構造器
private HungrySingleton() {
}
// 2.類內部建立物件,因為步驟3是static的,
// 所以例項物件是static的
private final static HungrySingleton instance;
static {
instance = new HungrySingleton();
}
//3.對外提供一個獲取物件的方法,
// 因為呼叫方式的目的就是為了獲取物件,
// 所以該方法應該是static的。
public static HungrySingleton getInstance() {
return instance;
}
}
複製程式碼
小結
該方式只是將物件的建立放在靜態程式碼塊中,其優點和缺點與第一種方式完全一樣。 總結:這種單例模式可以使用,但是可能造成記憶體的浪費。(同第一種)
懶漢式(執行緒不安全)
該方式的主要思想就是為了改善餓漢式的缺點,通過懶載入(在使用的時候再去載入),達到節約記憶體的目的。 由於無法使用new,你應該考慮提供一個獲取單例物件的方式給別人。
思路
1.將構造器私有化(防止外部new,但是對反射還是有侷限) 2.類的內部建立物件,懶載入,在使用的時候才去載入 3.對外提供一個獲取例項靜態的public方法
程式碼實現:
public class Singleton3 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
class LazySingleton {
//1.私有化構造器
private LazySingleton() {}
//2.類的內部宣告物件
private volatile static LazySingleton instance;
//3.對外提供獲取物件的方法
public static LazySingleton getInstance() {
//判斷類是否被初始化
if (instance == null) {
//第一次使用的時候,建立物件
instance = new LazySingleton();
}
return instance;
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LazySingleton instance = LazySingleton.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行程式後,發現了問題:
//執行結果:
執行緒Thread-0開始執行
執行緒Thread-1開始執行
執行緒Thread-1初始化物件1391273746
執行緒Thread-0初始化物件547686109
複製程式碼
小結
優點:起到了懶載入的作用,但是隻能在單執行緒情況下使用。 缺點:多執行緒下不安全,如果一個執行緒進入到if語句中阻滯(還未開始建立物件),另一執行緒進入並通過了if判斷,則會建立多個例項,這一點就違背了單例的目的。 結論:實際情況下,不要使用這種方式。
懶漢式(執行緒安全,同步方法)
思路
同上一中方式一樣,但是為瞭解決多執行緒安全問題,使用同步方法。
程式碼演示:
public class Singleton4 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
class LazySingleton {
//1.私有化構造器
private LazySingleton() {}
//2.類的內部宣告物件
private volatile static LazySingleton instance;
//3.對外提供獲取物件的方法
public synchronized static LazySingleton getInstance() {
//判斷類是否被初始化
if (instance == null) {
//第一次使用的時候,建立物件
instance = new LazySingleton();
}
return instance;
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LazySingleton instance = LazySingleton.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行結果如下所示:
執行緒Thread-1開始執行
執行緒Thread-0開始執行
執行緒Thread-0初始化物件681022576
執行緒Thread-1初始化物件681022576
複製程式碼
小結
優點:起到了懶載入的效果,同時,解決了執行緒安全問題。 缺點:效率低下,每次想要獲取物件的時候,去執行getInstance()都是通過同步方法。而且,初始化物件後,再次使用的時候,應該直接return這個物件。 總結:可以在多執行緒條件下使用,但是效率低下,不推薦。
懶漢式(執行緒安全,同步程式碼塊)
思路
同樣是為瞭解決多執行緒安全問題,不過採用的是同步程式碼塊。首先,最先想到的是:
1.將getInstance()方法體全部加上同步鎖。
程式碼實現:
public class Singleton5 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
//對getInstance()的方法體整體加同步程式碼塊
class LazySingleton {
//1.私有化構造器
private LazySingleton() {}
//2.類的內部宣告物件
private volatile static LazySingleton instance;
//3.對外提供獲取物件的方法
public static LazySingleton getInstance() {
//同步程式碼塊
synchronized (LazySingleton.class) {
//判斷類是否被初始化
if (instance == null) {
//第一次使用的時候,建立物件
instance = new LazySingleton();
}
}
return instance;
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LazySingleton instance = LazySingleton.getInstance();
// LazySingleton1 instance = LazySingleton1.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行的結果:
執行緒Thread-0開始執行
執行緒Thread-1開始執行
執行緒Thread-1初始化物件1419349448
執行緒Thread-0初始化物件1419349448
複製程式碼
這種方式的優缺點和同步方法一樣,能夠實現多執行緒安全,但是效率低下。那麼,能不能提高一下效率呢?我們發現,每次呼叫getInstance()的時候,都要進入同步程式碼塊,但是,一旦物件初始化後,第二次使用的時候,應該能夠直接獲取這個物件才對。 按照這個思路,對程式碼進行更改(為了說明這個,新建一個類LazySingleton1):
2.只在初始化物件部分加上同步鎖
程式碼實現:
//為了提高效率,通過if判斷,初始化之前進入同步鎖
class LazySingleton1 {
//1.私有化構造器
private LazySingleton1() {}
//2.類的內部宣告物件
private volatile static LazySingleton1 instance;
//3.對外提供獲取物件的方法
public static LazySingleton1 getInstance() {
//判斷類是否被初始化
if (instance == null) {
//第一次使用的時候,建立物件
synchronized (LazySingleton1.class) {
instance = new LazySingleton1();
}
}
return instance;
}
複製程式碼
將類TestClass的run()方法進行更改,獲取的例項改為LazySingleton1型別。程式碼看上去沒有問題,那麼執行效果如何呢:
//執行結果:
執行緒Thread-1開始執行
執行緒Thread-0開始執行
執行緒Thread-1初始化物件1368942806
執行緒Thread-0初始化物件1187311731
複製程式碼
那麼,我們發現,打臉了,多執行緒情況下,建立了兩個物件,並未達到單例的目的。
小結
- 對整個方法體加同步程式碼塊 可以達到要求,優缺點同同步方法。
- 只在初始化物件的程式碼新增同步鎖 不能滿足執行緒安全要求,實際工作中,不能使用這種方式。
懶漢式(執行緒安全,雙重檢查機制)
思路
針對懶漢式的多執行緒問題,我們可謂是操碎了心:同步方法可以解決問題,但是效率太低了;同步程式碼塊則根本不能保證多執行緒安全。如何能做到“魚和熊掌兼得”呢?既然同步程式碼塊的效率較好,那麼我們就針對這個方式進行改良:雙重檢查機制,即在getInstance()內進行兩次檢查,第一次通過if判斷後,初始化物件之前,進行同步並再次進行判斷。這樣做的目的:既能解決執行緒安全問題,同時避免第二次使用物件的時候還要執行同步的程式碼。
程式碼實現:
public class Singleton6 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
class LazyDoubleCheckSingleton {
//1.私有化構造器
private LazyDoubleCheckSingleton() {}
//2.類的內部宣告物件
private volatile static LazyDoubleCheckSingleton instance;
//3.對外提供獲取物件的方法
public static LazyDoubleCheckSingleton getInstance() {
//判斷類是否被初始化
if (instance == null) {
//第一次使用,通過if判斷
//加鎖
synchronized (LazyDoubleCheckSingleton.class) {
//拿到鎖後,初始化物件之前,再次進行判斷
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行結果如下所示:
//執行結果:
執行緒Thread-0開始執行
執行緒Thread-1開始執行
執行緒Thread-1初始化物件996963733
執行緒Thread-0初始化物件996963733
複製程式碼
小結
優點:
- 解決了上一種方式中的執行緒安全問題,同時實現了延遲載入的效果,節約記憶體;
- 第二次使用的時候,if判斷為false,直接返回建立好的物件,避免進入同步程式碼,提高了效率; 結論:推薦使用這種方式,實際工作中也比較常見這種方式。
靜態內部類
思路
為了實現多執行緒情況下安全,除了手工加鎖,還有別的方式。現在,我們採用靜態內部類的方式。這種方式利用了JVM載入類的機制來保證只初始化一個物件。 思路同樣是私有化構造器,對外提供靜態的公開方法;不同之處是,類的建立交給靜態內部類來時實現。
程式碼實現
public class Singleton7 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
class StaticInnerSingleton {
// 1.構造器私有化
private StaticInnerSingleton() {}
// 2.通過靜態內部類來初始化物件
private static class InnerClass {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
// 3.對外提供獲取物件的方法
public static StaticInnerSingleton getInstance() {
return InnerClass.INSTANCE;
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
StaticInnerSingleton instance = StaticInnerSingleton.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行結果:
執行緒Thread-0開始執行
執行緒Thread-1開始執行
執行緒Thread-0初始化物件1326533480
執行緒Thread-1初始化物件1326533480
複製程式碼
OK,我們發現,這種方式達到了預期的效果。
小結
優點:
- 這種靜態內部類的方式,通過類載入機制來保證了初始化例項時只有一個例項。
- 類的靜態屬性只有在第一次載入類的時候初始化,而JVM能保證執行緒安全,在類的初始化過程中,只有一個執行緒能進入並完成初始化。
- 靜態內部類方式實現了懶載入的效果,這種方式不會在類StaticInnerSingleton載入的時候進行初始化,而是在第一次使用時呼叫getInstance()方法初始化,能夠起到節約內次的目的。
- 該方式的getInstance()方法,通過呼叫靜態內部類的靜態屬性返回例項物件,避免了每次呼叫時進行同步,效率高。 結論:執行緒安全,效率高,程式碼實現簡單,推薦使用。
列舉
思路
在靜態內部類的方式中,我們借用了JVM的類載入機制來實現了功能,同樣,還可以借用Java的列舉來實現單例模式。
public class Singleton8 {
public static void main(String[] args) {
TestThread testThread = new TestThread();
Thread thread = new Thread(testThread);
Thread thread1 = new Thread(testThread);
thread.start();
thread1.start();
}
}
enum EnumSingleton {
INSTANCE;
public void sayHi() {
System.out.println("Hi," + INSTANCE);
}
}
class TestThread implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
EnumSingleton instance = EnumSingleton.INSTANCE;
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance.hashCode());
}
}
複製程式碼
執行結果如下:
執行緒Thread-0開始執行
執行緒Thread-1開始執行
執行緒Thread-1初始化物件1134798663
執行緒Thread-0初始化物件1134798663
複製程式碼
小結
優點:
- 這中方式需要在JDK1.5以上的版本中使用,利用列舉來實現單例模式。能避免多執行緒同步問題。
- 能防止反序列化重新建立新的物件。
- 能防止反射機制來破斷單例模式。 在《Effective Java》中提到了這種方式,其作者推薦。 結論:推薦使用。
使用容器來建立單例
思路
我們可以先初始化單例物件,通過容器來管理,然後在使用的時候從容器中獲取物件。
程式碼實現:
class ContainSingleton {
private ContainSingleton() {}
private static Map<String,Object> singletonMap = new HashMap<>();
public static Object getInstance(String key) {
return singletonMap.get(key);
}
public void putInstance(String key,Object instance) {
if (StringUtils.isNotEmpty(key) && instance != null) {
if (!singletonMap.containsKey(key)) {
singletonMap.put(key,instance);
}
}
}
}
複製程式碼
小結
這種單例模式是有一定的安全隱患的,如果你多個執行緒去建立例項,並且key相同,是有可能建立多個例項的。這種形式,建議在使用的時候,先去使用一個執行緒初始化資料後再使用。
執行緒池實現單例模式
思路
思路也前面的幾種形式一樣,無非就是用執行緒池來建立物件而已。
程式碼實現
class ThreadLocalSingleton {
//私有化構造器
private ThreadLocalSingleton() {}
//類的內部建立單例物件
private static final ThreadLocal<ThreadLocalSingleton> instanceThreadLocal =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
// 獲取物件的方法
public static ThreadLocalSingleton getInstance() {
return instanceThreadLocal.get();
}
}
複製程式碼
但是,這種形式的單例模式是要帶引號的。為什麼這麼說呢?寫一個程式碼測試一下吧:
class TestClass implements Runnable {
@Override
public void run() {
System.out.println("執行緒" + Thread.currentThread().getName() + "開始執行");
try {
//為了演示多執行緒情況
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println("執行緒" + Thread.currentThread().getName() + "初始化物件" + instance);
}
}
public class Singleton10 {
public static void main(String[] args) {
TestClass testClass = new TestClass();
Thread t1 = new Thread(testClass);
Thread t2 = new Thread(testClass);
t1.start();
t2.start();
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
}
}
複製程式碼
OK,我們發現了,多執行緒下建立了不同的物件,但是,對於同一執行緒,你多次獲取的物件始終是同一個。
小結
這種形式的單例模式,和之前的懶漢式加鎖的形式不一樣,加同步鎖的思路是犧牲時間(效率)來實現;這種形式是保證同一執行緒中的單例, 屬於犧牲空間來實現。
單例模式的序列化漏洞
在上面的列舉類的總結中,我們提高列舉方式能夠避免反序列化物件的時候重新建立新的物件(反序列化漏洞),那麼什麼是反序列化漏洞呢?Java物件進行反序列化的時候會通過反射機制來建立例項,反射機制的存在使得我們可以越過Java本身的靜態檢查和型別約束,在執行期直接訪問和修改目標物件的屬性和狀態。這裡理解的不是很準確,有錯誤的話請指出。
程式碼演示:
public class Test {
public static void main(String[] args) throws IOException,ClassNotFoundException {
// HungrySingleton instance = HungrySingleton.getInstance();
// //序列化
// ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
// oos.writeObject(instance);
//
// //反序列化
// ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
// HungrySingleton newInstance = (HungrySingleton) ois.readObject();
LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
oos.writeObject(instance);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
LazyDoubleCheckSingleton newInstance = (LazyDoubleCheckSingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
class HungrySingleton implements Serializable {
private static final long serialVersionUID = -4913346286867374832L;
//1.私有化構造器
private HungrySingleton() {
}
// 2.類內部建立物件,因為步驟3是static的,
// 所以例項物件是static的
private final static HungrySingleton instance;
static {
instance = new HungrySingleton();
}
//3.對外提供一個獲取物件的方法,
// 因為呼叫方式的目的就是為了獲取物件,
// 所以該方法應該是static的。
public static HungrySingleton getInstance() {
return instance;
}
//解決單例模式的反序列化漏洞
// public Object readResolve() {
// return instance;
// }
}
class LazyDoubleCheckSingleton implements Serializable {
private static final long serialVersionUID = -8459475238793042042L;
//1.私有化構造器
private LazyDoubleCheckSingleton() {}
//2.類的內部宣告物件
private volatile static LazyDoubleCheckSingleton instance;
//3.對外提供獲取物件的方法
public static LazyDoubleCheckSingleton getInstance() {
//判斷類是否被初始化
if (instance == null) {
//第一次使用,通過if判斷
//加鎖
synchronized (LazyDoubleCheckSingleton.class) {
//拿到鎖後,初始化物件之前,再次進行判斷
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
// public Object readResolve() {
// return instance;
// }
}
複製程式碼
這裡,我們分別提供了懶漢式和餓漢式(雙重檢查)來驗證這個現象。執行後會報錯,實現Serializable介面後能夠正常執行,結果如下:
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@6d03e736
false
複製程式碼
建立了兩個物件,沒有實現多執行緒安全。首先說明一下解決方案,然後再講解一下原理。我們發現餓漢式還是懶漢式都新增了一個方法readResolve(),將註釋取消後,再次執行的結果如下:
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
true
複製程式碼
奇蹟出現了,只是增加一個方法,情況完全不同了。那麼背後的原理是什麼呢?我們通過debug來講解:
1.在23行打一個斷點,進入並進入該方法: 2.我們發現,該方法首先是進行一些判斷,然後執行readObject0()方法,進入該方法檢視://該方法完成程式碼
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:
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);
}
}
複製程式碼
我們發現,該方法還是對傳入的物件進行一些判斷,在這裡,我們匹配到TC_OBJECT,執行對應的方法。 3.進入該方法:
4.進一步檢視: 我們看到一個名為resolveEx的屬性,說明很接近了。 5.繼續往下除錯: 我們發現,這三個條件都滿足,因為我們在LazyDoubleCheckSingleton類中定義了readResolve()方法。 6.if判斷通過,進入到下一個方法: 7.在該方法中,我們發現經過一些條件判斷後,通過反射方式來呼叫我們在類LazyDoubleCheckSingleton中新定義的方法readResolve():- 如果我們沒有新增這個方法,反射的時候會新建一個LazyDoubleCheckSingleton物件,並將其返回;
- 當我們新增這個readResolve()的時候,反射的時候還是會建立一個新的物件,但是,返回的是我們在readResolve()中的定義的返回物件。從而達到了多執行緒安全的目的。
單例模式的反射漏洞
除了反序列化漏洞,單例模式還有反射漏洞。下面介紹一下: 通過反射,能夠破壞單例模式,進而生成多個物件。
先來一個例子,以餓漢式為例:
class HungrySingleton {
private HungrySingleton() {}
private final static HungrySingleton instance = new HungrySingleton();
public static HungrySingleton getInstance() {
return instance;
}
}
public static void main(String[] args) throws Exception {
//測試,餓漢式
Constructor<HungrySingleton> constructor = HungrySingleton.class
.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
複製程式碼
執行一下,就能發現,生成了兩個例項,破壞了單例模式。同樣的情況,也會發生在靜態內部類、懶漢式中。
解決方案
- 餓漢式、靜態內部類: 直接改造一下構造器即可,防止生成多個物件。
private HungrySingleton() {
if (instance != null) {
throw new RuntimeException("禁止反射機制生成例項");
}
}
複製程式碼
靜態內部類同理。
- 懶漢式: 當你採用懶漢式的時候,關於防止反射攻擊,我是比較悲觀的。當然,解決問題的思路和餓漢式一樣,但是效果卻不盡人意。程式碼演示如下: 首先,改造構造器。
private HungrySingleton() {
if (instance != null) {
throw new RuntimeException("單例構造器禁止反射機制呼叫");
}
}
複製程式碼
但是,當你先執行getInstance()方法來生成例項的時候,問題能夠解決,可以當你先通過反射來生成物件的時候,就出問題了:
這時,你的執行結果就如下圖所示: 怎麼辦?有人說,新增一個變數,在構造器中根據變數的值該判斷,但是,這種方式其實沒啥用。因為同樣可以通過反射機制該修改屬性值。 在這裡,再一次想起神奇的列舉類,既能防止反序列化漏洞,又能防止反射漏洞,推薦大家使用。單例模式在框架原始碼中的使用
jdk中的使用案例
例如Runtime類,使用的就是單例模式的餓漢式(Runtime類在lang包中,在JVM執行的時候就被載入)來實現:
還有Desktop類,使用的就是單例模式的容器模式結合同步鎖來實現的:Spring中單例模式的應用
Spring單例Bean與單例模式的區別:它們關聯的環境不一樣,單例模式是指在一個JVM程式中僅有一個例項,而Spring單例是指一個Spring Bean容器(ApplicationContext)中僅有一個例項。
當你配置一個bean為單例的時候(預設就是singleton),在獲取物件的時候,spring會讀取判斷為true,然後如果這個物件已經建立好則直接返回,否則就呼叫方法getEarlySingletonInstance()來建立物件(其原始碼為第二張圖片)。總結
- 單例模式保證了 系統記憶體中該類只存在一個物件,節省了系統資源,對於一些需要頻繁建立銷燬的物件,使用單例模式可以提高系統效能。
- 當想例項化一個單例類的時候,必須要記住使用相應的獲取物件的方法,而不是使用new。
- 單例模式使用的場景:需要頻繁的進行建立和銷燬的物件、建立物件時耗時過多或耗費資源過多(即:重量級物件), 但又經常用到的物件、工具類物件、頻繁訪問資料庫或檔案的物件(比如資料來源、 session工廠等)。