//一.單例模式的簡要概述
1.單例模式被破壞的情況
暴力反射:在私有構造方法中,判非空時,直接拋出例外,禁止使用構造
序列化和反序列化:添加一個readResolve方法,回傳單例物件
Unsafe破壞:目前沒有解決策略
2.列舉單例是一種餓漢式的單例,不會受反序列化破壞,不會被暴力反射破壞,但unsafe束手無策,
3.普通懶漢式不能保證多執行緒安全
嘗試在獲取實體的整個方法上加synchronized關鍵字,但是在第一次獲取之后的效率較低,
4.雙重檢驗鎖(DCL)
兩次判空分別保證高效和單例、執行緒安全
volatile修飾單例物件保證 多執行緒下變數的可見性和有序性,能夠有效地防止指令重排(Jvm的優化手段)
5.靜態內部類
//進化史——普通餓漢式、列舉餓漢式、普通懶漢式、雙檢鎖懶漢式、靜態內部類懶漢式
二.jdk中的單例模式
System類中呼叫exit、gc方法本質都是呼叫了Runtime類,而Runtime是一個餓漢式單例物件
System類中的成員變數Console物件——雙檢鎖懶漢式
Collections集合工具類中有很多單例物件
//三.五種單例模式的實作
public class ExplainSingleton {
public static void main(String[] args) {
//測驗列舉類實作單例模式
System.out.println(SingletonObject5.INSTANCE==SingletonObject5.INSTANCE);
SingletonObj1 singletonObj1 = SingletonObj1.getInstance();
new Thread(){//匿名內部類是區域內部類的一種簡化形式.本質上是一個物件,是實作了該介面或繼承了該抽象類的子類物件.
@Override
public void run() {
SingletonObj1 singletonObj11 = SingletonObj1.getInstance();
System.out.println(singletonObj1==singletonObj11);
}
}.start();
SingletonObject2 singletonObject2 = SingletonObject2.getSingletonObject2();
new Thread(() -> {
SingletonObject2 singletonObject21 = SingletonObject2.getSingletonObject2();
System.out.println(singletonObject21==singletonObject2);
}).start();
}
}
//1.懶漢式——保證執行緒安全
class SingletonObj1 {
//私有化成員變數,禁止外類直接通過類獲取,然后對該變數進行參考的修改
private static SingletonObj1 singletonObj1;
//覆寫默認的構造方法,提供私有構造方法
private SingletonObj1(){
}
//靜態方法
public synchronized static SingletonObj1 getInstance(){
// 靜態方法可以被呼叫多次,每次呼叫都會執行方法體中的代碼,但是,如果一個類中有靜態代碼塊(static block),
// 那么這個代碼塊只會在類加載的時候執行一次,并且在所有物件中全域共享,靜態代碼塊通常用來初始化一些靜態變數或者做一些只需要執行一次的操作
// 總之,靜態方法可以被多次呼叫,但是靜態代碼塊只會被執行一次,
if(singletonObj1==null){
singletonObj1= new SingletonObj1();
}
return singletonObj1;
}
}
//2.餓漢式
//餓漢式單例的寫法適用于單例物件較少的情況,這樣寫可以保證絕對的執行緒安全,執行效率比較高,
// 但是缺點也很明顯,餓漢式會在類加載的時候就將所有單例物件實體化,這樣系統初始化的時候會造成大量的記憶體浪費.
// 從而導致系統的記憶體不可控,換句話說就是不管物件用不用,物件都已存在,占用記憶體,
class SingletonObject2{
//加載類的時候,舊產生單例物件.由于類加載只會有一次,故只會產生唯一的一個單例物件
private static SingletonObject2 singletonObject2=new SingletonObject2();
//構造私有化后,就不允許通過new產生物件,故只能通過 類獲取,所以相關的成員變數和方法都要有 static關鍵字
private SingletonObject2(){
}
public static SingletonObject2 getSingletonObject2(){
return singletonObject2;
}
}
//3.雙重校驗鎖.volatile 和 synchronized
//雙重判空
//特點為 懶初始化、執行緒安全、實作難度復雜、在多執行緒情況下能保持高性能,
class SingletonObject3{
/*
當一個變數被宣告為 volatile 時,意味著它的值可能會在程式的執行程序中被意外地修改,
例如在多執行緒或中斷處理程式中,這樣,編譯器就不能對該變數進行優化,以免導致代碼的行為與預期不符,
volatile 關鍵字通常被用于以下情況:
1.訪問硬體暫存器或設備狀態,這些狀態可能會隨時被修改,
2.在多執行緒環境中共享變數,以確保執行緒間的可見性,
3.在信號處理程式中訪問變數,以避免編譯器對變數的優化導致的問題,
需要注意的是,雖然使用 volatile 可以避免編譯器的優化,但它并不能保證執行緒安全和資料一致性,
因此在多執行緒環境中,還需要使用其他同步機制(CAS、Unsafe、AtomicInteger)來確保資料的正確性,
*/
//當一個變數被宣告為 volatile 時,volatile 變數的寫操作會立即重繪到主記憶體,
// 而讀操作會從主記憶體中讀取最新值,而非從本地執行緒快取中讀取,
//因此,當多個執行緒同時訪問一個共享的 volatile 變數時,它們總是能夠看到最新的值,
//需要注意的是,volatile 變數并不能完全保證執行緒安全,因為它只保證了可見性和有序性,而不保證原子性,
private volatile static SingletonObject3 singletonObject3;
private SingletonObject3(){
}
public static SingletonObject3 getSingletonObject3(){
if(singletonObject3==null){
//在多執行緒的競爭下,如果第一次沒有其他執行緒進行修改,則使用重量級鎖synchronized創建單例物件,
//synchronized 的使用可以保證執行緒安全,避免多個執行緒同時對共享資源進行修改而導致的資料不一致問題,
//需要注意的是,synchronized 的過度使用可能會導致程式性能下降,因此在使用時應該考慮到性能和實際需求,
synchronized (SingletonObject3.class){
if(singletonObject3==null){
singletonObject3=new SingletonObject3();
}
}
}
return singletonObject3;
}
}
//4.靜態內部類
//實作懶加載,內部類只加載一次,執行緒安全
//靜態內部類的加載是在程式中呼叫靜態內部類的時候加載的,和外部類的加載沒有必然關系,
//但是在加載靜態內部類的時候 發現外部類還沒有加載,那么就會先加載外部類.
//加載完外部類之后,再加載靜態內部類(初始化靜態變數和靜態代碼塊etc)
//如果在程式中單純的使用外部類,并不會觸發靜態內部類的加載
//類的加載時機:(暫時的認知里是四種)
//1.new 一個類的時候,
//2.呼叫類內部的 靜態變數,
//3.呼叫類的靜態方法,
//4.呼叫類的 靜態內部類
class SingletonObject4{
private SingletonObject4(){
//為了防止暴力反射破壞私有構造方法,可以直接在獲取構造方法時,直接拋出例外
throw new RuntimeException("不允許呼叫構造方法");
}
public SingletonObject4 getInstance(){
//外部類可以直接訪問內部類的
return SingletonObjectInner.instance;
}
private static final class SingletonObjectInner{
private static final SingletonObject4 instance=new SingletonObject4();
}
}
//5.列舉式
//JAVA規定:不允許通過反射呼叫構造方法的類---列舉
//列舉類有以下特點:
//1.列舉常量在列舉類中是唯一的,并且是不可改變的,
//2.列舉常量可以具有屬性、方法和建構式,
//3.列舉常量可以作為引數傳遞給方法或建構式,
//4.列舉常量可以使用switch陳述句進行比較,
//5.列舉可以實作介面,但是不能繼承其他類,
//6.列舉可以定義在類內部或外部,但是不能在方法內部定義,
//7.列舉常量通常使用大寫字母表示,并且使用下劃線分隔單詞,
//8.列舉類可以具有靜態方法和靜態屬性,
//9.列舉類可以實作Serializable和Comparable介面,
//10.列舉類可以使用valueOf()方法將一個字串轉換為列舉常量,
enum SingletonObject5 {
//1.列舉是一種特殊的類,每個列舉常量都是該類的一個實體,
//列舉實際上是一種語法糖,列舉類的每一個實體static final修飾的,只會在類加載時初始化一次,因此為單例,并且由JVM來保證執行緒安全,
// 列舉類實作單例模式的優勢是:
// - 簡潔易讀,不需要額外的代碼來保證單例,
// - 可以防止懶漢模式下的雙重檢查鎖定(DCL)問題,因為列舉型別在類加載時就已經初始化了,
//2.列舉為什么是單例
// 因為當一個類為enum的時候,其會被編譯為public static final T extends Enum,
// 因為是final修飾的,所以其首先是不能被繼承的,其次,Enum類中只有一個構造,其原始碼的注釋解釋:唯一的建構式,程式員無法調起此建構式(protected修飾),
// 它只能供編譯器回應列舉型別宣告發出的代碼使用,
INSTANCE;
//3.列舉實作單例模式特點之——天然不受序列化影響
// 首先,列舉都是默認繼承自java中Enum類的,列舉類實作Serializable介面,但在列舉類中禁用了readObject等一系列方法(通過直接拋例外),
// 我們知道,如果一個類實作Serializable介面,那么就不可能是單例,因為每次呼叫readObject方法都會回傳一個新的實體,
// 所以,完全可以通過序列化來破壞單例,但是列舉類有其自己的一套序列化方式,因此其禁用readObject方法,所以,不會因為序列化而破壞單例,
//4.列舉實作單例模式特點之——天然禁止暴力反射
// 列舉避免了反射破壞單例,因為列舉型別沒有構造方法,且來自父類Enum類的構造方法無法繼承,故無法被反射創建,
//5.列舉類為什么沒有公有的構造方法
// 列舉沒有構造方法,是因為列舉類是一種特殊的類,它的實體是固定的,并且由JVM在類加載時創建,
// 如果列舉有公有的構造方法,那么就可以在外部創建新的列舉實體,這樣就破壞了列舉的唯一性和不變性,
// 所以,列舉類只能有私有的構造方法,這樣就保證了只有在列舉類內部才能創建列舉實體,
public void whateverMethod() {
//do something
}
}
//單例模式的擴展——序列化問題
//創建完單例物件之后,有時候我們會使用序列化將物件寫入磁盤,當下次使用時再從磁盤中反序列化轉化為記憶體物件,這樣也會破壞單例模式,
//那么如何保證在序列化的情況下保證單例呢?很簡單,只需要增加readResolve方法,
//在jdk原始碼中,規定了這個方法已經約定方法名稱readResolve.這時候執行類中的readResolve方法,直接回傳已經創建的實體,
//即在原始碼中,只要實作序列化介面的單例類擁有一個名為readResolve的回傳單例物件的私有方法,那么反序列化后的結果變成了在單例類中已創建的實體物件,
class HungrySingleton implements Serializable {
private static final HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
private Object readResolve(){
return instance;
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/547442.html
標籤:Java
