程式設計師必備介面測試偵錯工具:
推薦學習:《》
單元素的列舉型別經常成為實現 Singleton 的最佳方法 。
什麼是單例?就一條基本原則,單例物件的類只會被初始化一次。在 Java 中,我們可以說在 JVM 中只存在該類的唯一一個物件範例。在 Android 中,我們可以說在程式執行期間,該類有且僅有一個物件範例。
單例模式的簡單實現步驟:
構造方法私有,保證無法從外部通過 new 的方式建立物件。
對外提供獲取該類範例的靜態方法。
類的內部建立該類的物件,通過第 2 步的靜態方法返回。
按照上述步驟寫下你認為比較嚴謹的單例模式,然後看看你所寫下的單例能否滿足以下條件:
涉及到並行三要素:原子性、可見性、有序性
//JAVA實現public class SingleTon { //第三步建立唯一範例
private static SingleTon instance = new SingleTon();
//第一步構造方法私有
private SingleTon() {
}
//第二步暴露靜態方法返回唯一範例
public static SingleTon getInstance() { return instance;
}
}//Kotlin實現object SingleTon
登入後複製
優點:設計簡單 ,解決了多執行緒範例化的問題。
缺點:在虛擬機器器載入SingleTon類的時候,將會在初始化階段為類靜態變數賦值,也就是在虛擬機器器載入該類的時候(此時可能並沒有呼叫 getInstance 方法)就已經呼叫了 new SingleTon();
建立了該物件的範例,之後不管這個範例物件用不用,都會佔據記憶體空間。
//JAVA實現public class SingleTon { //建立唯一範例
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() { //延遲初始化 在第一次呼叫 getInstance 的時候建立物件
if (instance == null) {
instance = new SingleTon();
} return instance;
}
}//Kotlin實現class SingleTon private constructor() { companion object { private var instance: SingleTon? = null
get() { if (field == null) {
field = SingleTon()
} return field
} fun get(): SingleTon{ return instance!!
}
}
}
登入後複製
優點:設計也是比較簡單的,和餓漢式不同,當這個Singleton被載入的時候,被static修飾的靜態變數將會被初始化為null,這個時候並不會佔用記憶體,而是當第一次呼叫getInstance方法的時候才會被初始化範例物件,按需建立。
缺點:在單執行緒環境下是沒有問題的,在多執行緒環境下,會產生執行緒安全問題。在有兩個執行緒同時 執行到了 instane == null這個語句,並且都通過了,那他們就會都各自範例化一個物件,這樣就又不是單例了。
如何解決懶漢式在多執行緒環境下的多範例問題?
靜態內部類
//JAVA實現public class SingleTon {
private static class InnerSingleton{ private static SingleTon singleTon = new SingleTon();
} public SingleTon getInstance(){ return InnerSingleton.singleTon;
}
private SingleTon() {
}
}//kotlin實現class SingleTon private constructor() {
companion object { val instance = InnerSingleton.instance
} private object InnerSingleton { val instance = SingleTon()
}
}
登入後複製
直接同步方法
//JAVA實現public class SingleTon { //建立唯一範例
private static SingleTon instance = null;
private SingleTon() {
}
public static synchronized SingleTon getInstance() { if (instance == null) {
instance = new SingleTon();
} return instance;
}
}//Kotlin實現class SingleTon private constructor() { companion object { private var instance: SingleTon? = null
get() { if (field == null) {
field = SingleTon()
} return field
} @Synchronized
fun get(): SingleTon{ return instance!!
}
}
}
登入後複製
優點:加鎖只有一個執行緒能範例該物件,解決了執行緒安全問題。
缺點:對於靜態方法而言,synchronized關鍵字會鎖住整個 Class,每次呼叫getInstance方法都會執行緒同步,效率十分低下,而且當建立好範例物件之後,也就不必繼續進行同步了。
備註:此處的synchronized保證了操作的原子性和記憶體可見性。
同步程式碼塊(雙重檢鎖方式DCL)
//JAVA實現 public class SingleTon { //建立唯一範例
private static volatile SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() { if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
} return instance;
}
}//kotlin實現class SingleTon private constructor() { companion object { val instance: SingleTon by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
SingleTon()
}
}
}
或者class SingleTon private constructor() { companion object { @Volatile private var instance: SingleTon? = null
fun getInstance() =
instance ?: synchronized(this) {
instance ?: SingleTon().also { instance = it }
}
}
}
登入後複製
優點:新增了一個同步程式碼塊,在同步程式碼塊中去判斷範例物件是否存在,如果不存在則去建立,這個時候其實就完全可以解決問題了,因為雖然是多個執行緒去獲取範例物件,但是在同一個時間也只會有一個執行緒會進入到同步程式碼塊,那麼這個時候建立好物件之後,其他執行緒即便再次進入同步程式碼塊,由於已經建立好了範例物件,便直接返回即可。但是為什麼還要在同步程式碼塊的上一步再次去判斷instance為空呢?這個是由於當我們建立好範例物件之後,直接去判斷此範例物件是否為空,如果不為空,則直接返回就好了,就避免再次進去同步程式碼塊了,提高了效能。
缺點:無法避免暴力反射建立物件。
備註:此處的volatile發揮了記憶體可見性及防止指令重排序作用。
public enum SingletonEnum { INSTANCE; public static void main(String[] args) { System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
}
}
登入後複製
列舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性。(關於Android使用列舉會產生效能問題的說法,這應該是Android 2.x系統之前記憶體緊張的時代了,現在已經Android 13了,相信某些場合列舉所帶來的便利遠遠大於這點所謂的效能影響)
以最初的DCL為測試案例,看看如何進行反射攻擊及又如何在一定程度上避免反射攻擊。
反射攻擊程式碼如下:
public static void main(String[] args) {
SingleTon singleton1 = SingleTon.getInstance();
SingleTon singleton2 = null;
try {
Class<SingleTon> clazz = SingleTon.class;
Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("singleton1.hashCode():" + singleton1.hashCode());
System.out.println("singleton2.hashCode():" + singleton2.hashCode());
}
登入後複製
執行結果:
singleton1.hashCode():1296064247
singleton2.hashCode():1637070917
登入後複製
通過執行結果發現通過反射破壞了單例。 如何保證反射安全呢?只能以暴制暴,當已經存在範例的時候再去呼叫建構函式直接丟擲異常,對建構函式做如下修改:
public class SingleTon { //建立唯一範例
private static volatile SingleTon instance = null;
private SingleTon() { if (instance != null) { throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
public static SingleTon getInstance() { if (instance == null) {
synchronized (SingleTon.class) {
if (instance == null) {
instance = new SingleTon();
}
}
} return instance;
}
}
登入後複製
此時可防禦反射攻擊,丟擲異常如下:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:45)
at com.imock.demo.TestUtil.main(TestUtil.java:33)
Caused by: java.lang.RuntimeException: 單例構造器禁止反射呼叫
at com.imock.demo.SingleTon.<init>(SingleTon.java:16)
... 6 more Exception in thread "main" java.lang.NullPointerException
at com.imock.demo.TestUtil.testSingleInstance(TestUtil.java:49)
at com.imock.demo.TestUtil.main(TestUtil.java:33)
Process finished with exit code 1
登入後複製
然後我們把上述測試程式碼修改如下(調換了singleton1的初始化順序)
:
public static void main(String[] args) {
SingleTon singleton2 = null;
try {
Class<SingleTon> clazz = SingleTon.class;
Constructor<SingleTon> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
singleton2 = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("singleton2.hashCode():" + singleton2.hashCode());
SingleTon singleton1 = SingleTon.getInstance(); //調換了位置,在反射之後執行
System.out.println("singleton1.hashCode():" + singleton1.hashCode());
}
登入後複製
執行結果:
singleton2.hashCode():1296064247
singleton1.hashCode():1637070917
登入後複製
發現此防禦未起到作用。
缺點:
如何避免序列化攻擊?只需要修改反序列化的邏輯就可以了,即重寫 readResolve()
方法,使其返回統一範例。
protected Object readResolve() { return getInstance();
}
登入後複製
脆弱不堪的單例模式經過重重考驗,進化成了完全體,延遲載入,執行緒安全,反射及序列化安全。簡易程式碼如下:
餓漢模式
public class SingleTon { private static SingleTon instance = new SingleTon();
private SingleTon() { if (instance != null) { throw new RuntimeException("單例構造器禁止反射呼叫");
}
} public static SingleTon getInstance() { return instance;
}
}
登入後複製
靜態內部類
public class SingleTon {
private static class InnerStaticClass{ private static SingleTon singleTon = new SingleTon();
} public SingleTon getInstance(){ return InnerStaticClass.singleTon;
}
private SingleTon() { if (InnerStaticClass.singleTon != null) { throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
}
登入後複製
懶漢模式
public class SingleTon { //建立唯一範例
private static SingleTon instance = null;
private SingleTon() { if (instance != null) { throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
public static SingleTon getInstance() { //延遲初始化 在第一次呼叫 getInstance 的時候建立物件
if (instance == null) {
instance = new SingleTon();
} return instance;
}
}
登入後複製
缺點:
(列舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性,底層實現比如newInstance方法內部判斷列舉拋異常)
推薦學習:《》
以上就是一起來分析java設計模式之單例的詳細內容,更多請關注TW511.COM其它相關文章!