單例設計模式
什麼是單例
保證一個類只有一個實例,並且提供一個訪問該全局訪問點
單例應用場景
-
Windows的Task Manager(任務管理器)就是很典型的單例模式(這個很熟悉吧),想想看,是不是呢,你能打開兩個windows task manager嗎? 不信你自己試試看哦~
-
windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統運行過程中,回收站一直維護着僅有的一個實例。
-
網站的計數器,一般也是採用單例模式實現,否則難以同步。
-
應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只能有一個實例去操作,否則內容不好追加。
-
Web應用的配置對象的讀取,一般也應用單例模式,這個是由於配置文件是共享的資源。
-
數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源。數據庫軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因爲何用單例模式來維護,就可以大大降低這種損耗。
-
多線程的線程池的設計一般也是採用單例模式,這是由於線程池要方便對池中的線程進行控制。
-
操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。
-
HttpApplication 也是單位例的典型應用。熟悉ASP.Net(IIS)的整個請求生命週期的人應該知道HttpApplication也是單例模式,所有的HttpModule都共享一個HttpApplication實例.
單例優缺點
優點:
-
在單例模式中,活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就 防止其它對象對自己的實例化,確保所有的對象都訪問一個實例
-
單例模式具有一定的伸縮性,類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。
-
提供了對唯一實例的受控訪問。
-
由於在系統內存中只存在一個對象,因此可以 節約系統資源,當 需要頻繁創建和銷燬的對象時單例模式無疑可以提高系統的性能。
-
允許可變數目的實例。
-
避免對共享資源的多重佔用。
缺點:
-
不適用於變化的對象,如果同一類型的對象總是要在不同的用例場景發生變化,單例就會引起數據的錯誤,不能保存彼此的狀態。
-
由於單利模式中沒有抽象層,因此單例類的擴展有很大的困難。
-
單例類的職責過重,在一定程度上違背了“單一職責原則”。
-
濫用單例將帶來一些負面問題,如爲了節省資源將數據庫連接池對象設計爲的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;如果實例化的對象長時間不被利用,系統會認爲是垃圾而被回收,這將導致對象狀態的丟失。
單例創建方式
-
餓漢式:類初始化時,會立即加載該對象,線程天生安全,調用效率高。
-
懶漢式: 類初始化時,不會初始化該對象,真正需要使用的時候纔會創建該對象,具備懶加載功能。
-
靜態內部方式:結合了懶漢式和餓漢式各自的優點,真正需要對象的時候纔會加載,加載類是線程安全的。
-
枚舉單例: 使用枚舉實現單例模式 優點:實現簡單、調用效率高,枚舉本身就是單例,由jvm從根本上提供保障!避免通過反射和反序列化的漏洞, 缺點沒有延遲加載。
-
雙重檢測鎖方式 (因爲JVM本質重排序的原因,可能會初始化多次,不推薦使用)
餓漢式
- 類初始化時創建對象,不管需不需要實例對象,都會創建
- 不存在線程安全問題,因爲實例是在類創建和初始化時創建,是由類加載器完成的,類加載器是線程安全的
書寫
(1)構造器私有化
(2)自行創建,並且用靜態變量保存
(3)向外提供這個實例
(4)強調這是一個靜態的,可以使用final修飾
// 簡潔直觀
public class Singleton1 {
private Singleton1(){}
public static final Singleton1 INSTANCE = new Singleton1();
}
枚舉式
// 使用枚舉實現單例模式 優點:實現簡單、枚舉本身就是單例,由jvm從根本上提供保障!避免通過反射和反序列化的漏洞 缺點沒有延遲加載
public class User {
public static User getInstance() {
return SingletonDemo04.INSTANCE.getInstance();
}
private static enum SingletonDemo04 {
INSTANCE;
// 枚舉元素爲單例
private User user;
private SingletonDemo04() {
System.out.println("SingletonDemo04");
user = new User();
}
public User getInstance() {
return user;
}
}
public static void main(String[] args) {
User u1 = User.getInstance();
User u2 = User.getInstance();
System.out.println(u1 == u2);
}
}
靜態代碼塊方式
// 適合複雜的實例化
class A {
static final String info = "password";
}
public class Singleton3_test {
public String info;
private Singleton3(String info) {
this.info = info;
}
static {
// 從其他地方獲取的值
String val = A.info;
INSTANCE = new Singleton3_test(val);
}
public static final Singleton3_test INSTANCE;
@Override
public String toString() {
return "Singleton3{" +
"info='" + info + '\'' +
'}';
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
懶漢式
延遲創建實例對象,需要時纔去創建對象,否則不創建
書寫
(1)構造器私有化
(2)用一個靜態變量保存這個唯一的實例
(3)提供一個靜態方法,獲取這個實例對象
public class SingletonLan1 {
private SingletonLan1(){}
private static SingletonLan1 instance;
public synchronized static SingletonLan1 getInstance(){
if (instance == null){
instance = new SingletonLan1();
}
return instance;
}
}
// 測試
public class Lan_Test1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<SingletonLan1> call = new Callable<SingletonLan1>() {
@Override
public SingletonLan1 call() throws Exception {
return SingletonLan1.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<SingletonLan1> f1 = es.submit(call);
Future<SingletonLan1> f2 = es.submit(call);
SingletonLan1 s1 = f1.get();
SingletonLan1 s2 = f2.get();
es.shutdown();
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
雙重檢測鎖方式
public class SingletonLan2 {
private SingletonLan2(){}
private static SingletonLan2 instance;
public static SingletonLan2 getInstance(){
if (instance == null){
synchronized (SingletonLan2.class){
if (instance == null) {
instance = new SingletonLan2();
}
}
}
return instance;
}
}
// 測試
public class Lan_Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<SingletonLan2> call = new Callable<SingletonLan2>() {
@Override
public SingletonLan2 call() throws Exception {
return SingletonLan2.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<SingletonLan2> f1 = es.submit(call);
Future<SingletonLan2> f2 = es.submit(call);
SingletonLan2 s1 = f1.get();
SingletonLan2 s2 = f2.get();
es.shutdown();
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
}
靜態內部類方式
- 在內部類被加載和創建時,纔去創建實例對象
- 靜態內部類不會隨着外部類的加載和初始化而初始化,它是要單獨加載和初始化的
- 因爲是內部類創建和初始化時,創建的對象,所以不存在線程安全問題
// 相比上一種方法,簡潔許多
public class SingletonLan3 {
private SingletonLan3(){}
private static class Inner {
private static final SingletonLan3 INSTANCE = new SingletonLan3();
}
public static SingletonLan3 getInstance(){
return Inner.INSTANCE;
}
}
單例防止反射漏洞攻擊
// 在構造函數中,只能允許初始化化一次即可。
public SingletonDemo04 {
private static boolean flag = false;
private SingletonDemo04() {
if (flag == false) {
flag = !flag;
} else {
throw new RuntimeException("單例模式被侵犯!");
}
}
public static void main(String[] args) {
}
}
如何選擇單例創建方式
- 如果不需要延遲加載單例,可以使用枚舉或者餓漢式,相對來說枚舉性好於餓漢式。
- 如果需要延遲加載,可以使用靜態內部類或者懶漢式,相對來說靜態內部類好於懶漢式。