單例模式指的是確保一個類在任何情況下都絕對只有一個實例,並提供一個全局訪問點。單例模式是創建型模式。
1、餓漢模式
餓漢模式指的是在類加載的時候立即進行初始化,並創建單例對象。
優點:不需要加鎖,執行效率高,實現簡單;
缺點:初始化後可能有很長一段時間不會使用,導致內存的浪費。
適用場景:適用於單例對象較少的情況。
代碼示例:
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
2、懶漢模式
懶漢模式指的是在外部類調用的時候纔會加載。
2.1 雙重檢查
double check指的是,先判斷靜態變量實例是否已經存在,如果存在,則直接返回結果;如果不存在,先加鎖,然後再檢查實例是否存在,如果存在則直接返回結果,如果不存在,再去創建實例。
public class LazySingleton {
// 防止指令重排
private static volatile LazySingleton INSTANCE;
private LazySingleton() {};
public static LazySingleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (LazySingleton.class) {
// double check
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new LazySingleton();
return INSTANCE;
}
}
}
2.2 防止指令重排
需要注意的是,上面的實例還給靜態變量INSTANCE加了修飾符volatile,用來防止指令重排導致的,調用方獲取到未完全初始化完成的實例。
正常情況下,INSTANCE = new LazySingleton();任務分成三步:
- 分配對象的內存空間;
- 初始化對象;
- 設置單例指向剛分配的內存地址。
但是由於2、3步沒有必然的先後順序,所以會被指令重排優化爲:
- 分配對象的內存空間;
- 設置單例指向剛分配的內存地址;
- 初始化對象。
如果初始化對象花費的時間較長,就會返回給調用方一個未完全初始化的實例。詳情可以參考volatile原理。
3、反射破壞單例
可以通過反射的方式強制調用單例類的構造器,構造新的實例:
public static void main(String[] args) {
LazySingleton lazySingleton = LazySingleton.getInstance();
// reflect
LazySingleton lazySingleton1 = reflectLazySingleton();
System.out.println(lazySingleton == lazySingleton1);
}
private static LazySingleton reflectLazySingleton() {
try {
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> constructor = lazySingletonClass.getConstructor(null);
constructor.setAccessible(true);
return constructor.newInstance();
} catch (Exception e) {
return null;
}
}
解決方案:在構造器中校驗靜態變量是否爲空,如果不爲空則打斷構造實例的過程:
private LazySingleton() {
if (INSTANCE != null) {
throw new RuntimeException("不允許創建多個實例");
}
}
4、序列化破壞單例
4.1 ObjectInputStream反序列化原理
ObjectInputStream反序列化實例時,通過readObject()方法最終去調用readOrdinaryObject()方法,部分代碼如下:
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
上述邏輯,主要是判斷該類是否有構造器,如果有則構造新的實例,否則返回空。
繼續往下看,會發現:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
代碼中會判斷目標類中是否存在readResolve()方法,如果有的話就調用readResolve()方法獲取實例,並返回該實例。
4.2 避免ObjectInputStream反序列化破壞單例
根據上述分析可知,可以通過實現readResolve()方法來避免ObjectInputStream反序列化破壞單例:
public Object readResolve() {
return getInstance();
}
4.3 注意實現
1、實際上,在過程中,還是創建了新的實例,只不過實例沒有被返回,該實例在反序列化後會被回收;
2、這個只能解決二進制序列化導致的單例被破壞的問題,並不能解決json序列化導致的單例被破壞的問題;
3、如果使用了第三節中防止反射破壞單例的過程,則會導致該實例無法反序列化。
5、枚舉式單例
5.1 實現
枚舉式單例模式,指的是將通過枚舉類來實現單例:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
5.2 防止序列化破壞
ObjectInputStream在反序列化枚舉類時,是通過在readObject()方法中調用readObject0(),然後在readObject0()中調用readEnum()來實現的。readEnum()中的部分代碼:
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = 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);
}
}
再去看Enum.valueOf()的源碼:
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);
}
其中,enumType.enumConstantDirectory()是從class對象中返回的實例參數,是一個Map,所以枚舉類的反序列化是通過反序列化的內容(name)從內存中尋找已經存在的枚舉值,所以不會創建新的實例。
5.3 防止反射破壞單例
反射通過構造器調用newInstance()方法來構造新的實例,newInstance()方法的源碼如下:
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;
}
可以看到,如果類型修飾符是枚舉,會直接打斷並拋出異常。
6、容器式單例
容器式單例用於單例很多的情況,可以統一管理:
public class ContainerSingleton {
private ContainerSingleton() {
}
private static final ConcurrentHashMap<String, Object> MAP = new ConcurrentHashMap<>();
public static Object getInstance(String type) {
if (MAP.containsKey(type)) {
return MAP.get(type);
}
try {
Object value = Class.forName(type).newInstance();
if (value == null) {
return null;
}
Object newValue = MAP.putIfAbsent(type, value);
return newValue == null ? value : newValue;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
這裏通過判斷ConcurrentHashMap的putIfAbsent的返回值是否爲空,來確定是否有其他線程向MAP中放入了新的實例,避免了加鎖帶來的開銷。當然也有缺點,如果併發級別非常非常高,可能會出現多個線程對同一個key創建實例,如果value的內存開銷很大的話,這時對內存造成一定的壓力。