您好,登錄后才能下訂單哦!
這篇文章主要介紹了java設計模式之怎么實現單例模式的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇java設計模式之怎么實現單例模式文章都會有所收獲,下面我們一起來看看吧。
單元素的枚舉類型經常成為實現 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
發現此防御未起到作用。
缺點:
如果反射攻擊發生在正常調用getInstance之前,每次反射攻擊都可以獲取單例類的一個實例,因為即使私有構造器中使用了靜態成員(instance) ,但單例對象并沒有在類的初始化階段被實例化,所以防御代碼不生效,從而可以通過構造器的反射調用創建單例類的多個實例;
如果反射攻擊發生在正常調用之后,防御代碼是可以生效的;
如何避免序列化攻擊?只需要修改反序列化的邏輯就可以了,即重寫 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;
}
}
缺點:
如果反射攻擊發生在正常調用getInstance之前,每次反射攻擊都可以獲取單例類的一個實例,因為即使私有構造器中使用了靜態成員(instance) ,但單例對象并沒有在類的初始化階段被實例化,所以防御代碼不生效,從而可以通過構造器的反射調用創建單例類的多個實例;
如果反射攻擊發生在正常調用之后,防御代碼是可以生效的。
(枚舉實現單例是最為推薦的一種方法,因為就算通過序列化,反射等也沒辦法破壞單例性,底層實現比如newInstance方法內部判斷枚舉拋異常)
Java主要應用于:1. web開發;2. Android開發;3. 客戶端開發;4. 網頁開發;5. 企業級應用開發;6. Java大數據開發;7.游戲開發等。
關于“java設計模式之怎么實現單例模式”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“java設計模式之怎么實現單例模式”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。