目錄
寫在前面
本文從最基礎的餓漢式及懶漢式demo進行引入,通過jdk源碼分別分析了:反射及反序列化破壞單例原理、readResolve()如何防止反序列化破壞單例、枚舉式單例的優點及如何防止反射及反序列化破壞、以及spring容器式單例思想詳解。
餓漢式單例模式:
一般形式
/**
* 優點:執行效率高,性能高,沒有任何的鎖
* 缺點:某些情況下,可能會造成內存浪費
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
靜態代碼塊
/**
* 靜態代碼塊
*/
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
懶漢式單例模式:
雙重檢查鎖
/**
* 優點:性能高了,線程安全了
* 缺點:可讀性難度加大,不夠優雅,並且加鎖會產生性能問題
*/
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//檢查是否要阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//檢查是否要重新創建實例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
//指令重排序的問題
}
}
}
return instance;
}
}
靜態內部類
/*
優點:寫法優雅,利用了Java本身語法特點,性能高,避免了內存浪費,不能被反射破壞
缺點:不優雅
*/
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允許非法訪問");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
反射破壞單例
public class ReflectTest {
public static void main(String[] args) {
try {
//獲取單例類的class及構造器
Class<?> clazz = LazyDoubleCheckSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
//設置強制訪問
c.setAccessible(true);
//實例化兩次
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
//分別打印
System.out.println(instance1);
System.out.println(instance2);
//false
System.out.println(instance1 == instance2);
}catch (Exception e){
e.printStackTrace();
}
}
}
輸出結果:
序列化破壞單例:
//一個單例對象創建好後,有時候需要將對象序列化然後寫入磁盤,下次使用時再從磁盤中讀取對象進行反序列化,然後將其轉化爲內存對象。反序列化後的對象會重新分配內存,即重新創建
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SeriableSingleton implements Serializable {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
運行結果爲:
添加readResolve()方法
保證序列化不會破壞單例demo及運行結果
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class SeriableSingleton implements Serializable {
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
運行結果
原理分析
寫在前面:雖然解決了單例模式被破壞的問題,但是實際上實例化了兩次,只不過新創建的對象沒有被返回,根據如下調用棧即可探明真相readObject()【ObjectInputStream】->readObject0(false)【ObjectInputStream】->readOrdinaryObject(unshared)【ObjectInputStream】
- 先從demo中轉到如下代碼:
s1 = (SeriableSingleton)ois.readObject();
- 進入:readObject()【爲節省篇幅去掉了對此分析無關代碼】
//去掉無用代碼
public final Object readObject() {
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;
}
- 進入:readObject0(false)【爲節省篇幅去掉了對此分析無關代碼】
private Object readObject0(boolean unshared) throws IOException {
//去掉無用代碼
switch (tc) {
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);
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
}
- 進入:readOrdinaryObject(unshared)【爲節省篇幅去掉了對此分析無關代碼】
- 從該方法中我們可以看到先通過實現反序列化後的對象,如果單例對象中定義了readResolve()方法,則對前面生成的對象進行覆蓋,來保證單例。
- 實際上實例化了兩次,只不過第二次實例化的對象沒有被返回而已
private Object readOrdinaryObject(boolean unshared) {
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
//1. 實例化反序列化對象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//如果單例對象存在readResolve(),則對第一步【1. 實例化反序列化對象】產生的對象進行覆蓋
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//進行覆蓋
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
}
return obj;
}
註冊試單例模式
枚舉式單例模式
寫在前面:枚舉式單例模式,無法通過反射及反序列化來破壞單例,是實現單例模式最爲優良的方式,並且《Effective Java》一書也推薦使用枚舉來實現單例
代碼實現
public class EnumSingletonTest {
public static void main(String[] args) {
//構建兩個實例對象
System.out.println("測試枚舉類型單例--start");
EnumSingleton instance1 = EnumSingleton.getInstance();
EnumSingleton instance2 = EnumSingleton.getInstance();
System.out.println("是否爲同一對象:" + (instance1 == instance2));
System.out.println();
try {
System.out.println("測試反射能夠破壞單例--start");
//測試通過反射能否破壞單例
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
Object o = c.newInstance();
}catch (Exception e){
System.out.println("發生異常,不允許通過反射構造Enum實例"+e.getMessage());
}finally {
System.out.println();
}
try {
EnumSingleton enumSingleton1 = EnumSingleton.getInstance();
EnumSingleton enumSingleton2 = null;
System.out.println("測試反序列化能夠破壞單例--start");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
oos.writeObject(enumSingleton1);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));
enumSingleton2 = (EnumSingleton)ois.readObject();
System.out.println("是否爲同一對象:" + (enumSingleton1 == enumSingleton2));
}catch (Exception e){
System.out.println("發生異常,不允許通過反序列化破壞單例"+e.getMessage());
}
}
}
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;}
}
運行結果
原理詳解
-
爲什麼想要通過反射破環單例時,獲取構造方法時要傳入兩個參數呢?
clazz.getDeclaredConstructor(String.class,int.class);
查看java.lang.Enum類的源碼即可發現,其只含有這一個構造器 -
Enum類型是如何防止反射破壞單例的,我們進入Constructor的newInstance()方法即可探明真相
-
Enum類型是如何防止反序列化破壞單例的。
- 寫在前面:因爲是通過類名及類對象找到唯一的枚舉類,所以不會產生多實例
- 先通過此調用棧readObject()【ObjectInputStream】->readObject0(false)【ObjectInputStream】->readEnum(unshared)【ObjectInputStream】進入readEnum方法。可參見詳解readResolve()方法時的原理分析
- ObjectInputStream.readEnum()方法如下,詳見代碼:
Enum.valueOf((Class)cl, name);
private Enum<?> readEnum(boolean unshared) throws IOException {
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
@SuppressWarnings("unchecked")
//通過類名及類對象找到唯一的枚舉類
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
- Enum.valueOf((Class)cl, name);方法如下:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
//獲取存儲的Enum類對象
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);
}
容器式單例模式
寫在前面:Enum實現單例雖有有衆多優點,但是當單例數量衆多時卻不方便管理。仿照spring思想,如果通過一個容器同一存儲則更方便管理,但是該方法實現的單例線程不安全也容易被破壞。
demo及運行結果
public class ContainerSingletonTest {
public static void main(String[] args) {
Object instance1 = ContainerSingleton.getInstance("singleton.container.ContainerSingleton");
Object instance2 = ContainerSingleton.getInstance("singleton.container.ContainerSingleton");
System.out.println(instance1 == instance2);
}
}
class ContainerSingleton {
private ContainerSingleton(){}
//通過容器管理所有的實例
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
spring框架思想驗證
總結
- 餓漢式單例:類加載即初始化、線程安全,但是可以通過反射和反序列化破壞其單例,防止通過反射破壞其單例的方式爲:當實例化之後再調用構造函數時拋出異常
- 懶漢式單例:第一次調用進行初始化、線程不安全,需要通過雙重檢查鎖來實現線程安全,其他與餓漢式單例相同。
- 反射破壞單例原理:雖然構造器設置爲私有,但是可以通過設置強制訪問來調用其構造函數,具體爲:
c.setAccessible(true);
- 序列化破壞單例原理:反序列化後的對象會重新分配內存,即重新創建
- readResolve()方法防止反序列化破壞單例原理:在反序列化調用readObject()方法中,會先反序列化一個實例,再進行判斷是否定義了該方法,如果定義了該方法,則將剛纔反序列化生成的對象進行覆蓋。其實實際上實例化了兩次,只不過新創建的對象沒有被返回
- 枚舉式單例模式:枚舉式單例模式,無法通過反射及反序列化來破壞單例。無法通過反射破壞單例是因爲jdk底層做了限制,當發現反射調用的是枚舉的構造器時,會拋出“”異常;無法反序列化來破環單例是因爲反序列化時如果該Enum類已被實例化則通過類名及類對象找到該枚舉類並返回,所以不會產生多實例。是實現單例模式最爲優良的方式,並且《Effective Java》一書也推薦使用枚舉來實現單例
- 容器式單例模式:方便於管理衆多的單例對象,但會出現線程安全問題,也會出現反射和反序列化破壞其單例的現象,不過spring中的對象管理通過該方式
▄█▀█●各位同仁,如果我的代碼對你有幫助,請給我一個贊吧,爲了下次方便找到,也可關注加收藏呀
如果有什麼意見或建議,也可留言區討論