單例模式學習筆記
-
- 懶漢方式
- 餓漢方式
- 靜态内部類
- 防止反射破壞單例
學習視訊連接配接
單例模式也就是全局隻提供一個執行個體,對于為什麼要用單例模式,目前我的認知就是單例模式可以節省重複建立執行個體的過程,節約時間。
實作單例模式首先要有一個獲得執行個體的方法getInstance,還要将無參構造函數私有化。
public class LazySinglton {
private static LazySinglton instance;
private LazySinglton(){
}
public LazySinglton getInstance(){
return 執行個體;
}
}
下面分别用三種方式實作單例模式并對比三種方式之間的差别。
懶漢方式
懶漢方式也就是當我們使用的時候才去建立單例。
最簡單的懶漢方式:
public class LazySinglton {
private static LazySinglton instance;
private LazySinglton(){
}
public static LazySinglton getInstance(){
if(instance == null )
instance = new LazySinglton();
return instance;
}
public static void main(String[] args) {
LazySinglton t1 = LazySinglton.getInstance();
LazySinglton t2 = LazySinglton.getInstance();
System.out.println(t1);
System.out.println(t2);
}
}
輸出結果:
[email protected]
[email protected]
但是該種方式存在問題,如果有多個線程同時進入if(lazySinglton == null )那麼每個線程都會對instance進行執行個體化,會破壞單例。
使用兩個線程獲得執行個體。
//由于線程執行的很快,是以可以在getInstance()方法中使用Thread.sleep函數休眠
new Thread(()->{
try {
LazySinglton t1 = LazySinglton.getInstance();
System.out.println(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{
try {
LazySinglton t2 = LazySinglton.getInstance();
System.out.println(t2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
輸出結果:
[email protected]
[email protected]
如何防止?在java中可以使用同步代碼塊防止單例被破壞。
在getInstance()方法中使用synchronized修飾建立執行個體的代碼。
synchronized方法使用(csdn部落客)通俗易懂
public static LazySinglton getInstance() throws InterruptedException {
synchronized (LazySinglton.class){
if(instance == null )
Thread.sleep(200);
instance = new LazySinglton();
}
return instance;
}
上述方式每次調用getInstance()方法都需要經過同步代碼塊,影響效率,可以采取雙重檢查的方式,提高效率。
該方式需要将 instance使用volatile修飾,原因可自行了解。
if(singleton == null){
//可能會有很多線程同時進入第一個if,然後由于synchronized 導緻隻有一個線程能進入下面代碼塊,直到進入的線程釋放資源
//這時候其他線程進入下面代碼塊,發現singleton已經被指派,
//再之後的線程則會被第一個if篩掉,不會重複的走同步代碼塊
synchronized (lazySingleton.class){
if(singleton == null)
singleton = new lazySingleton();
}
}
return singleton;
餓漢方式
餓漢方式也就是在初始化過程中就進行執行個體化。
懶漢可以了解為餓了的時候才去取面包吃。
餓漢可以了解為無論餓不餓都先把面包拿過來。
public class HungrySingleton {
//餓漢模式,在類初始化的時候執行個體化單例,
//由于是靜态建立對象的,也就是會在連接配接階段給該成員賦預設值,
//在初始化階段賦初始值,借助虛拟機的加載機制實作線程安全(jvm的類加載就是線程安全的)
private static final HungrySingleton single = new HungrySingleton();
public HungrySingleton getInstance(){
return single;
}
}
餓漢方式比懶漢方式對比:
- 懶漢式時間換空間,每次調用getInstance都需要判斷是否已經被執行個體,會降低效率。
- 餓漢式空間換時間,在類加載過程中會建立所有執行個體,調用getInstance方法時不需要判斷,效率較高。
靜态内部類
package Singleton;
public class InnerClassSingleton {
private InnerClassSingleton(){
}
private static class InnerClassHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
public static InnerClassSingleton getInstace(){
return InnerClassHolder.instance;
}
public static void main(String[] args) {
InnerClassSingleton t1 = InnerClassSingleton.getInstace();
InnerClassSingleton t2 = InnerClassSingleton.getInstace();
System.out.println(t1);
System.out.println(t2);
}
}
輸出結果:
[email protected]
[email protected]
靜态内部類的優點:
外部類加載時不會立即加載内部類,也就是說JVM加載類InnerClassSingleton時并不會立即加載InnerClassHolder類,那麼使用靜态内部類就不會像餓漢模式一樣占用記憶體,而是在調用getInstance時才會加載InnerClassHolder,這樣又可以實作懶漢模式的懶加載,又可以通過JVM保證線程的安全。
防止反射破壞單例
不通過方法,而是通過反射擷取構造器建立執行個體可以破壞以上三種方式的單例,以靜态内部類為例
//修改靜态内部類例子中的main函數
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
InnerClassSingleton t1 = InnerClassSingleton.getInstace();
InnerClassSingleton t2 = InnerClassSingleton.getInstace();
System.out.println(t1);
System.out.println(t2);
Class singleClass = InnerClassSingleton.class;
Constructor declaredConstructor = singleClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerClassSingleton reflect = (InnerClassSingleton) declaredConstructor.newInstance();
System.out.println(reflect);
}
[email protected]
[email protected]
[email protected]
如何防止反射破壞單例?
在懶漢模式和靜态内部類方法中可以在構造函數中再次進行判斷,以靜态内部類為例,在構造函數中判斷intance是否已經被執行個體化,如果是傳回異常。
private InnerClassSingleton(){
if(InnerClassHolder.instance != null){
throw new RuntimeException("不要試圖使用反射破壞單例");
}
}