1.1.3.1 問題演示
破壞單例模式:
使上面定義的單例類(Singleton)可以創建多個對象,枚舉方式除外。有兩種方式,分別是序列化和反射。
-
序列化反序列化
Singleton類:
public class Singleton implements Serializable { //私有構造方法 private Singleton() {} //定義一個靜態內部類 private static class SingletonHolder { //在內部類中聲明並初始化外部類的對象 private static final Singleton INSTANCE = new Singleton(); } //提供公共的訪問方式 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
Test類:
public class Test { public static void main(String[] args) throws Exception { //往文件中寫對象 //writeObject2File(); //從文件中讀取對象 Singleton s1 = readObjectFromFile(); Singleton s2 = readObjectFromFile(); //判斷兩個反序列化後的對象是否是同一個對象 System.out.println(s1 == s2); } private static Singleton readObjectFromFile() throws Exception { //創建對象輸入流對象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt")); //第一個讀取Singleton對象 Singleton instance = (Singleton) ois.readObject(); return instance; } public static void writeObject2File() throws Exception { //獲取Singleton類的對象 Singleton instance = Singleton.getInstance(); //創建對象輸出流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt")); //將instance對象寫出到文件中 oos.writeObject(instance); } }
上面代碼運行結果是
false
,表明序列化和反序列化已經破壞了單例設計模式。 -
反射
Singleton類:
public class Singleton { //私有構造方法 private Singleton() {} private static volatile Singleton instance; //對外提供靜態方法獲取該對象 public static Singleton getInstance() { if(instance != null) { return instance; } synchronized (Singleton.class) { if(instance != null) { return instance; } instance = new Singleton(); return instance; } } }
Test類:
public class Test { public static void main(String[] args) throws Exception { //獲取Singleton類的字節碼對象 Class clazz = Singleton.class; //獲取Singleton類的私有無參構造方法對象 Constructor constructor = clazz.getDeclaredConstructor(); //取消訪問檢查 constructor.setAccessible(true); //創建Singleton類的對象s1 Singleton s1 = (Singleton) constructor.newInstance(); //創建Singleton類的對象s2 Singleton s2 = (Singleton) constructor.newInstance(); //判斷通過反射創建的兩個Singleton對象是否是同一個對象 System.out.println(s1 == s2); } }
上面代碼運行結果是
false
,表明反射已經破壞了單例設計模式
注意:枚舉方式不會出現這兩個問題。
1.1.3.2 問題的解決
-
序列化、反序列方式破壞單例模式的解決方法
在Singleton類中添加
readResolve()
方法,在反序列化時被反射調用,如果定義了這個方法,就返回這個方法的值,如果沒有定義,則返回新new出來的對象。Singleton類:
public class Singleton implements Serializable { //私有構造方法 private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } //對外提供靜態方法獲取該對象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } /** * 下面是爲了解決序列化反序列化破解單例模式 * 當進行反序列化時,會自動調用該方法,將該方法的返回值直接返回 */ private Object readResolve() { return SingletonHolder.INSTANCE; } }
Test類
public class Client { public static void main(String[] args) throws Exception { //writeObject2File(); readObjectFromFile(); readObjectFromFile(); } //從文件讀取數據(對象) public static void readObjectFromFile() throws Exception { //1,創建對象輸入流對象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\a.txt")); //2,讀取對象 Singleton instance = (Singleton) ois.readObject(); System.out.println(instance); //釋放資源 ois.close(); } //向文件中寫數據(對象) public static void writeObject2File() throws Exception { //1,獲取Singleton對象 Singleton instance = Singleton.getInstance(); //2,創建對象輸出流對象 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\a.txt")); //3,寫對象 oos.writeObject(instance); //4,釋放資源 oos.close(); } }
可以看到打印出來的兩個地址是一樣的,說明已經解決單例模式被破壞的問題
那爲啥會自動調用readResolve這個方法呢?,實際上就是ObjectInputStream種的readObject方法在起作用
源碼解析:
ObjectInputStream類
public final Object readObject() throws IOException, ClassNotFoundException{ ... // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false);//重點查看readObject0方法 ..... } private Object readObject0(boolean unshared) throws IOException { ... try { switch (tc) { ... case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));//重點查看readOrdinaryObject方法 ... } } finally { depth--; bin.setBlockDataMode(oldMode); } } private Object readOrdinaryObject(boolean unshared) throws IOException { ... //isInstantiable 返回true,執行 desc.newInstance(),通過反射創建新的單例類, obj = desc.isInstantiable() ? desc.newInstance() : null; ... // 在Singleton類中添加 readResolve 方法後 desc.hasReadResolveMethod() 方法執行結果爲true if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { // 通過反射調用 Singleton 類中的 readResolve 方法,將返回值賦值給rep變量 // 這樣多次調用ObjectInputStream類中的readObject方法,繼而就會調用我們定義的readResolve方法,所以返回的是同一個對象。 Object rep = desc.invokeReadResolve(obj); ... } return obj; }
-
反射方式破解單例的解決方法
public class Singleton { private static boolean flag = false; //私有構造方法 private Singleton() { synchronized (Singleton.class) { //判斷flag的值是否是true,如果是true,說明非第一次訪問,直接拋一個異常,如果是false的話,說明第一次訪問 if (flag) { throw new RuntimeException("不能創建多個對象"); } //將flag的值設置爲true flag = true; } } //定義一個靜態內部類 private static class SingletonHolder { //在內部類中聲明並初始化外部類的對象 private static final Singleton INSTANCE = new Singleton(); } //提供公共的訪問方式 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
說明:
這種方式比較好理解。當通過反射方式調用構造方法進行創建創建時,直接拋異常。不運行此中操作。
接下來介紹一個在jdk中源碼中使用單例模式的一個類
1.1.4 JDK源碼解析-Runtime類
Runtime類就是使用的單例設計模式。
-
通過源代碼查看使用的是哪兒種單例模式
public class Runtime { private static Runtime currentRuntime = new Runtime(); /** * Returns the runtime object associated with the current Java application. * Most of the methods of class <code>Runtime</code> are instance * methods and must be invoked with respect to the current runtime object. * * @return the <code>Runtime</code> object associated with the current * Java application. */ public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {} ... }
從上面源代碼中可以看出Runtime類使用的是餓漢式(靜態屬性)方式來實現單例模式的。
-
使用Runtime類中的方法
public class RuntimeDemo { public static void main(String[] args) throws IOException { //獲取Runtime類對象 Runtime runtime = Runtime.getRuntime(); //返回 Java 虛擬機中的內存總量。 System.out.println(runtime.totalMemory()); //返回 Java 虛擬機試圖使用的最大內存量。 System.out.println(runtime.maxMemory()); //創建一個新的進程執行指定的字符串命令,返回進程對象 Process process = runtime.exec("ipconfig"); //獲取命令執行後的結果,通過輸入流獲取 InputStream inputStream = process.getInputStream(); byte[] arr = new byte[1024 * 1024* 100]; int b = inputStream.read(arr); System.out.println(new String(arr,0,b,"gbk")); } }
執行結果如下