單例模式的定義如下:
確定某一個類隻有一個執行個體,而且自行執行個體化并向整個系統提供這個執行個體。
單例類自身儲存它的唯一執行個體,這個類保證沒有其他執行個體可以被建立,并且它可以提供一個通路該執行個體的方法。
單例模式的一些特點:
構造方法私有化,防止外部通過通路構造方法建立對象;
提供一個全局方法使其單例對象被外部通路;
考慮多線程并發情況的單例唯一性。
單例模式的幾種實作方式:
懶漢式、餓漢式、内部類、注冊式、枚舉類
這裡強烈推薦的是内部類和枚舉類的實作方式。
餓漢式
在類加載的時候就立即建立單例對象。
- 優點:絕對的線程安全,無鎖,執行效率高;
- 缺點:即使單例在程式中一直用不到,也會在類加載的時候初始化,不管用或不用,都占據記憶體空間。
package com.faith.net;
/**
* 餓漢式
*/
public class Singleton {
private Singleton(){}
private static final Singleton hungry = new Singleton();
public static Singleton getInstance(){
return hungry;
}
}
懶漢式
當需要使用單例的時候才進行執行個體化。
package com.faith.net;
/**
* 懶漢式
*/
public class Singleton {
private Singleton(){}
private static Singleton lazy = null;
public static Singleton getInstance(){
if(lazy == null){
lazy = new Singleton();
}
return lazy;
}
}
需要注意的是以上擷取單例不是線程安全的。
- 用synchronized改版
package com.faith.net;
/**
* 懶漢式
*/
public class Singleton {
private Singleton(){}
private static Singleton lazy = null;
public static synchronized Singleton getInstance(){
if(lazy == null){
lazy = new Singleton();
}
return lazy;
}
}
通過synchronized關鍵字保證了線程安全,但這種排隊方式的處理在多線情況下效率較低。
雙重鎖定
雙重鎖定是懶漢式的一種實作方式,上面例子中synchronized關鍵字加在了方法上,導緻了每個線程都會排隊擷取對象。
雙重鎖定的方式是不讓線程每次都加鎖,而是隻在執行個體未被建立的情況下加鎖,同時也能保證線程安全,這種做法稱為雙重鎖定(Double-Check Locking)。
示例如下:
package com.faith.net;
import java.io.Serializable;
/**
* 單例類
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
内部類
這種方式使用了内部類的一些特性:
- 内部類隻有在外部類被調用的時候才會被加載;
- 内部類在方法調用之前完成初始化;
- 其次,類的加載機制保證了每個類隻會被加載一次。
這是非常推薦的一種方式,它綜合了懶漢式延遲加載和餓漢式線程安全的特性。
package com.faith.net;
/**
* 内部類方式
*/
public class Singleton {
private boolean initialized = false;
private Singleton(){}
/*
* static 保證單例共享,final保證方法不被重寫
*/
public static final Singleton getInstance(){
return LazyHolder.LAZY; // 在傳回結果以前,會先加載内部類
}
// 内部類
private static class Singleton{
private static final Singleton LAZY = new Singleton();
}
}
注冊式
注冊式是spring IOC容器使用的一種方式,通過将執行個體儲存到Map容器中實作。
package com.faith.net;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 注冊式
*/
public class BeanFactory {
private BeanFactory(){}
private static Map<String,Object> container = new ConcurrentHashMap<String,Object>();
public static synchronized Object getBean(String className){
if(!ioc.containsKey(className)){
Object obj = Class.forName(className).newInstance();
container.put(className,obj);
return obj;
}else{
return container.get(className);
}
}
}
枚舉類
枚舉有如下特性:
- 枚舉對象,例如下面的INSTANCE由枚舉機制保證了一定會是單例的;
- 枚舉的加載機制保證了線程安全;
- 枚舉保證了執行個體不會被反射破壞;
- 枚舉的對象隻有被使用時才會進行執行個體化,保證了延遲加載。
是以通過枚舉實作,也會保證代碼的高效、線程安全以及延遲加載的特性。
實作方式
- 傳回自身對象
package com.faith.net;
public enum Singleton {
INSTANCE; // 單例對象
// 這裡可以添加多個成員方法,例如
public void doSomething() {
// 省略具體代碼
}
}
- 傳回其他對象,例如ConcurrentHashMap
package com.faith.net;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public enum Singleton {
INSTANCE; // 枚舉對象,由于枚舉機制,這個也是單例的
private Map instance; // 需要做成單例的對象
Singleton() {
instance = new ConcurrentHashMap();
}
public Map getInstance() {
return instance;
}
}
反序列化對單例的破壞
對象的序列化指将對象轉化為位元組流;反序列化指将位元組流轉化為相應的對象。
反序列化的特點是,預設情況下,會根據位元組流建立一個新的對象。那麼這種特性會導緻破壞單例。如下:
- 建立單例類
package com.faith.net;
import java.io.Serializable;
/**
* 單例類
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 用戶端測試反序列化
package com.faith.net;
import java.io.*;
public class SerializableDemo1 {
public static void main(String[] args) throws Exception {
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
// 反序列化
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判斷
System.out.println(newInstance == Singleton.getSingleton());
}
}
輸出會為false。這種情況可以通過在單例類中添加readResolve方法解決,如下:
package com.faith.net;
import java.io.Serializable;
/**
* 單例類
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
因為java對象的反序列化通過ObjectInputputStream實作的,通過readObject方法讀取位元組流并建立新的對象。
但在readObject方法中有一個判斷,内容是判斷被反序列化的類中是否包含readResolve方法,如果包含readResolve方法,就直接傳回readResolve方法中的邏輯,是以readResolve能避免反序列化對單例模式的破壞。
反射對單例的破壞
懶漢式、餓漢式都可以被反射破壞,如下:
public static void main(String[] args) {
Class<?> clazz = Singleton.class;
//通過反射拿到私有的構造方法
Constructor c = clazz.getDeclaredConstructor(null);
//強制通路私有構造
c.setAccessible(true);
//調用了兩次構造方法,相當于new了兩次
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}
而枚舉可以避免被反射破壞,因為枚舉不能被反射方式通路。