Java常用設計模式-單例模式
Java Design Patterns:
建立型模式:工廠方法、抽象方法、建造者、原型、單例
結構型模式有:介面卡、橋接、組合、裝飾器、外觀、享元、代理
行為型模式有:責任鏈、命令、直譯器、迭代器、中介、備忘錄、觀察者、狀態、策略、模板方法、訪問者
常用設計模式:
單例模式、工廠模式、代理模式、策略模式&模板模式、門面模式、責任鏈模式、裝飾器模式、組合模式、builder模式
單例模式
簡介
確保一個類只有一個例項,並提供一個全域性訪問點
懶漢式:
/**
* 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
* 懶漢式:
* 是否 Lazy 初始化:是
* 是否多執行緒安全:否
*/
public class Signleton {
private static Signleton signleton;
private Signleton(){}
//可以透過synchronized關鍵字保證執行緒安全
public static Signleton getSignleton(){
if(signleton == null){
signleton = new Signleton();
}
return signleton;
}
}
餓漢式:
/**
* 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
* 餓漢式:
* 是否 Lazy 初始化:否
* 是否多執行緒安全:是
*/
class Signleton1{
private static Signleton1 signleton1 = new Signleton1();
private Signleton1(){}
public static Signleton1 getSignleton1(){
return signleton1;
}
}
懶漢式:解決反射、序列化反序列化問題
/**
* 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
* 懶漢式:
* 是否 Lazy 初始化:是
* 是否多執行緒安全:否
*/
public class Signleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Signleton signleton;
private Signleton() {
// 防止反射
if (signleton != null) {
throw new RuntimeException();
}
}
// 可以透過synchronized關鍵字保證執行緒安全
public static Signleton getSignleton() {
if (signleton == null) {
signleton = new Signleton();
}
return signleton;
}
/*
序列化:當一個物件被序列化時,Java 將該物件的狀態寫入一個位元組流。
反序列化:當位元組流被反序列化時,Java 將建立一個新的物件例項,並將位元組流中的資料填充到這個新例項中。
readResolve 方法:在物件被反序列化之後,Java 會呼叫這個方法。如果該方法存在,返回的物件將代替預設反序列化過程中建立的新物件。
*/
private Object readResolve() {
return signleton;
}
}
/**
* 反射測試
*/
@Test
public void test(){
//獲取單例
Signleton signleton = Signleton.getSignleton();
Signleton signleton1 = Signleton.getSignleton();
System.out.println(signleton.hashCode());
System.out.println(signleton1.hashCode());
//透過反射破壞單例
try {
Class<?> aClass = Class.forName("design.patterns.Signleton");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Signleton signleton2 = (Signleton) declaredConstructor.newInstance();
System.out.println(signleton2.hashCode());
} catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* 序列化測試
*/
@Test
public void test1(){
//獲取單例
Signleton signleton = Signleton.getSignleton();
Signleton signleton1 = Signleton.getSignleton();
System.out.println(signleton.hashCode());
System.out.println(signleton1.hashCode());
//序列化反序列化獲取物件
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:/signleton.ser"));
outputStream.writeObject(signleton1);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("D:/signleton.ser"));
Signleton signleton2 = (Signleton) inputStream.readObject();
System.out.println(signleton2.hashCode());
} catch (IOException | ClassNotFoundException e) {
// throw new RuntimeException(e);
e.printStackTrace();
}
}
懶漢式DCL(推薦):雙重檢查鎖定(Double-Checked Locking)是用於減少同步開銷,同時保證執行緒安全的一種最佳化方法。其核心思想是:在訪問共享資源時,先進行一次非同步的檢查,如果未初始化,再進入同步塊進行第二次檢查和初始化。這樣可以避免每次呼叫獲取例項方法時都需要進行同步,從而提升效能。
這裡也確保序列化安全。
/**
* 單例設計模式:確保一個類只有一個物件例項,並提供一個全域性訪問點
* 懶漢式:
* 是否 Lazy 初始化:是
* 是否多執行緒安全:否
*/
public class Signleton implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile Signleton signleton;
private Signleton() {}
public static Signleton getSignleton() {
if (signleton == null) {
synchronized(Signleton.class){
if(signleton == null){
signleton = new Signleton();
}
}
}
return signleton;
}
/*
序列化:當一個物件被序列化時,Java 將該物件的狀態寫入一個位元組流。
反序列化:當位元組流被反序列化時,Java 將建立一個新的物件例項,並將位元組流中的資料填充到這個新例項中。
readResolve 方法:在物件被反序列化之後,Java 會呼叫這個方法。如果該方法存在,返回的物件將代替預設反序列化過程中建立的新物件。
*/
private Object readResolve() {
return signleton;
}
}
場景
資源共享:避免頻繁的建立銷燬某個物件,造成存好。比如:日誌檔案。
控制資源:避免過多的物件產生,造成其他問題。比如網站的計數器。
應用場景:
-
日誌管理器:避免頻繁建立和銷燬日誌物件,確保日誌檔案只被一個例項操作,以便內容可以正確追加。
-
網站計數器:全域性唯一例項用於統計網站訪問次數,避免併發更新問題。
-
Windows 回收站:整個系統執行過程中,回收站一直維護著唯一的一個例項。
-
多執行緒的執行緒池:執行緒池需要方便控制池中的執行緒,單例模式確保執行緒池全域性唯一。
-
/** * 單例執行緒池 -- 應用場景 */ public class ThreadPool1 { private static ThreadPool1 threadPool; // 定義介面 private ExecutorService executorService; private ThreadPool1() { executorService = new ThreadPoolExecutor( 5, // 核心執行緒數 10, // 匯流排程數 60, TimeUnit.MILLISECONDS, // 存活時間和單位 new LinkedBlockingDeque<Runnable>(), // 用於儲存等待執行的任務的佇列 new ThreadFactory() { // 用於建立新執行緒的工廠 // 定義原子操作的 int 型別。它可以在多執行緒環境下安全地進行自增、自減等操作而不需要同步 private AtomicInteger threadNumber = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, "CustomThreadPool-thread-" + threadNumber.getAndIncrement()); // thread.setDaemon(true); // 設定為守護執行緒 thread.setPriority(Thread.NORM_PRIORITY); return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒絕策略,當任務佇列滿了且無法再接受任務時的處理策略 ); } public static synchronized ThreadPool1 getThreadPool() { if (threadPool == null) { threadPool = new ThreadPool1(); } return threadPool; } public void submitTask(Runnable runnable) { executorService.submit(runnable); } public void shutdown() { executorService.shutdown(); } } /** * 單例執行緒池測試 */ @Test public void test2(){ Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + " task is run"); }; // ThreadPool1.getThreadPool().submitTask(runnable); ThreadPool1 threadPool = ThreadPool1.getThreadPool(); threadPool.submitTask(runnable); System.out.println(threadPool.hashCode()); ThreadPool1 threadPool1 = ThreadPool1.getThreadPool(); threadPool1.submitTask(runnable); System.out.println(threadPool1.hashCode()); } //輸出: 158199555 158199555 CustomThreadPool-thread-2 task is run CustomThreadPool-thread-1 task is run
-
-
SpringBoot中的大多數容器管理的Bean都是單例的,這些bean是應用程式級別的單例,也就是說不同使用者共享同一個例項。比如@RestController、@Service、@Compoment、@Configuration註解修飾的類,預設都是單例。
優點
控制資源的使用:
- 例項控制:確保一個類只有一個例項,避免了多個例項導致的資源浪費。例如,在資料庫連線池或執行緒池的設計中,單例模式確保只建立一個連線池或執行緒池例項,從而控制資源的使用。
- 節省資源:減少了系統的開銷,避免了重複建立和銷燬物件的高昂成本。
全域性訪問點:
- 統一管理:透過提供一個全域性訪問點,可以方便地管理和訪問例項。比如,在日誌記錄系統中,透過單例模式可以確保所有日誌記錄都透過同一個例項進行處理,從而統一日誌的輸出格式和內容。
- 一致性:所有對該例項的操作都透過統一的介面進行,確保了資料的一致性和完整性。
易於擴充套件:
- 延遲例項化:懶漢式單例模式在首次使用時才建立例項,避免了不必要的資源浪費。這種延遲載入的特性也便於在系統啟動時減少初始化時間。
缺點
潛在的資源爭用:
- 資源競爭:如果單例類內部使用了共享資源,而這些資源在高併發場景下沒有妥善處理,可能導致資源競爭問題。例如,某個單例例項持有資料庫連線物件,在高併發請求下可能導致連線池枯竭。
單點故障:
- 故障影響範圍大:如果單例例項出現問題,整個系統的相關功能可能會受到影響。例如,日誌記錄系統的單例例項出現異常,可能導致整個系統無法正常記錄日誌。
難以測試:
- 測試複雜性:由於單例模式在整個應用程式中只有一個例項,單元測試時可能會導致測試的隔離性和獨立性變差。例如,一個單例類的狀態在多個測試方法之間共享,可能導致測試結果互相影響。
隱藏依賴關係:
- 依賴性不明確:單例模式透過全域性訪問點訪問例項,可能會導致類與類之間的依賴關係變得不清晰。例如,一個類可能隱式依賴於某個單例類的狀態,增加了系統的複雜性和維護成本。