文章目錄
單例模式
定義
指一個類只有一個實例,且這個類能自行創建這個實例的一種模式。
特點
- 單例類只有一個實例對象。
- 該單例對象必須由單例類自行創建。
- 單例類對外提供一個訪問該單例的全局訪問點。
單例類四大原則
- 構造私有
- 通過靜態方法或者枚舉返回實例
- 確保實例只有一個,尤其是多線程
- 確保反序列化時不會重新構建對象。
實現方式
單例模式實現方式有:
- 餓漢式
- 懶漢式(雙重鎖懶漢式)
- 靜態內部類模式
- 枚舉模式
餓漢式
在類被初始化時候就已經在內存中創建了對象,因此是以空間換時間,不存在線程安全問題
public class SingleTon {
//構造私有
private SingleTon() {}
private static SingleTon INSTANCE = new SingleTon();
//通過靜態方法返回實例
public static SingleTon getInstance() {
return INSTANCE;
}
}
懶漢式
懶漢式在方法被調用後才創建對象,因此是以時間換空間,在多線程環境下存在風險,可通過使用Double Check Lock雙重檢測鎖來確保多線程下安全。
public class SingleTon {
//構造私有
private SingleTon() {}
private volatile static SingleTon INSTANCE = null;
//通過靜態方法返回實例對象
public static SingleTon getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon();
}
return INSTANCE;
}
//通過靜態方法返回實例對象(內部使用雙重鎖來確保多線程安全)
public static SingleTon getInstance() {
if (INSTANCE == null) {
synchronized (SingleTon.class) {
if (INSTANCE == null) {
INSTANCE = new SingleTon();
}
}
}
return INSTANCE;
}
}
靜態內部類方式
外部類被加載時並不需要立即加載內部類,內部類不被加載則不會去初始化INSTANCE,故而不會佔用內存。
public class SingleTon {
//構造私有
private SingleTon() {}
//靜態內部類中創建單例對象
private static Build{
//靜態變量只會在類第一訪問時候被初始化(因此只會初始化一次)
private static SingleTon INSTANCE = new SingleTon();
}
//通過靜態方法返回對象實例
public static SingleTon getInstance(){
return Build.Instance;
}
}
靜態內部類優點:
- 外部類被加載時並不需要立即加載內部類,因此不會佔用內存。
- 當第一次調用getInstance()會導致jvm加載Build類,這種方法不僅能確保線程安全,也能保證單例唯一性,同時也延遲了類的實例化。靜態內部類如何實現線程安全的呢?@see擴展中的類加載時機
缺點:
- 無法傳遞參數
枚舉創建
public enum SingleTonEnum {
INSTANCE;
public void test(String name) {
System.out.println("name:" + name);
}
}
枚舉創建單例優點:
- 創建簡潔
- 線程安全
- 有效防止反射、序列化、反序列化對單例破壞
擴展
DCL(Double Check Lock)雙重鎖失效問題及解決方案
由於jvm存在亂序執行
功能,DCL也會出現線程不安全的情況,具體分析如下:
INSTANCE = new SingleTon();
這個步驟,其實在jvm裏面執行分爲三步
- 在堆內存開闢內存空間。
- 在堆內存中實例化SingleTon裏面的各個參數。
- 把對象指向堆內存空間。
由於jvm存在亂序執行功能,所以可能在2還沒執行時就先執行了3,如果此時在被切換到線程B,由於執行了3,INSTANCE已經非空了,會被直接拿出來用,這樣就會拋出異常,這就是著名的DCL失效問題。
解決方案
在JDK1.5之後官方發現這個問題後故而具體化了volatile
,即在JDK1.6及以後,只要定義爲private volatile static SingleTon INSTANCE = null就可以解決DCL失效問題,volatile
確保INSTANCE每次均在主內存中讀取(而不是使用一個緩存值),這樣雖然會犧牲一點效率,但也無傷大雅。
Volatile與Synchronzed區別
volatile與synchronized都是確保多線程安全的同步機制。
- synchronized時時阻塞式的同步鎖,在線程競爭激烈情況下會升級爲重量級鎖。
- volatile是jvm提供的輕量級同步機制。volatile修飾的變量每次操作都會立刻回寫到主內存中
產生線程安全的原因?
線程消耗的是CPU,CPU讀取數據順序爲
寄存器-高速緩存-主內存
在計算時候會將共享變量從主內存中拷貝到當前線程私有的工作內存中,當線程計算完成後,工作內存中的數據會在適當的時機寫回主內存(並不是立即回寫),如果多個線程併發訪問即會出現線程安全問題。
如何確保多線程安全?
java提供synchronized
和volatile
來確保多線程安全。
-
synchronized
java提供的阻塞式同步鎖,在多線程競爭激烈情況下會升級爲重量級鎖。
- 假如這個鎖被其他線程佔用,JVM就會把這個線程線程放進鎖池中,並且進入阻塞狀態,等到其他線程釋放了鎖之後,jvm會隨機從鎖池中取出一個線程,讓其擁有鎖,並且進入就緒狀態。
- 假如這個鎖沒有被佔用,本線程獲取鎖,開始執行同步代碼塊,在執行代碼塊時候,如果執行了鎖所屬對象的
wait()
方法,這個線程會釋放對象鎖,進入對象的等待池中。 - sleep與wait區別:sleep不會釋對象鎖,而wait則會釋放對象鎖。
-
volatile
jvm提供的輕量級同步機制。它的作用是在線程每次操作變量之後會立即回寫到主內存中,這樣對於其他線程來說達到可見性。
- 當線程回寫到主內存中之後,其他線程中的緩存失效。
- 當線程發現工作內存中的緩存失效後,就會重新從主內存中讀取變量的值,即可以獲取當前變量最新的值。
類加載機制
jvm對類的加載流程需要經過如下步驟
其中加載-驗證-準備-初始化-卸載
必須順序執行。
何時進行類的加載?
什麼情況下虛擬機需要開始加載一個類呢?虛擬機規範中並沒有對此進行強制約束,這點可以交給虛擬機的具體實現來自由把握。“一般是引用到某個類的時候jvm會開始進行加載”
何時進行類的初始化?
jvm對類的初始化場景有如下6種
- 創建類的實例
- 訪問類的靜態變量
- 訪問類的靜態方法
- 反射
- 初始化一個類時,父類還沒初始化,則先初始化父類
- 虛擬機啓動時候,聲明的main()方法所在的類
《深入理解JAVA虛擬機》中有這麼一句虛擬機會保證一個類的構造器(<clinit>())方法在多線程環境中被正確的加鎖和同步。如果多個線程同時去初始化一個類,那麼只會有一個線程去執行這類的構造器方法,其他線程都需要阻塞等待,直到活動線程執行構造器方法完畢
。如果構造器方法中有耗時操作,就有可能造成多個線程阻塞,(需要注意的是,其他線程雖然會阻塞,但如果執行構造器方法後,其他線程喚醒後不會在進入構造器方法。同一個加載器下,一個類型只會初始化一次。
因此,可以看出在靜態內部類中創建外部類實例是線程安全的,所以說靜態內部類形式的單例可保證線程安全,可能保證單例的唯一性,同時也延遲了單例實例化。
類的初始化和實例化的區別?
-
初始化
類的初始化是指爲類中的靜態變量賦予初始值的過程,是類聲明週期中的一個階段。
-
實例化
初始化是創建一個類的實例(對象)過程。
序列化反射對單例的破壞
/**
* 使用雙重校驗鎖方式實現單例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
public class SerializableDemo1 {
//爲了便於理解,忽略關閉流操作及刪除文件操作。真正編碼時千萬不要忘記
//Exception直接拋出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判斷是否是同一個對象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//false
在序列化過程中會通過反射調用無參數的構造方法創建一個新的對象,破壞了SingleTon的單例性。
解決方案
-
防止反射破壞單例模式
在單例對象的私有構造方法中進行攔截處理
//構造私有 private SingleTon() { if(INSTANCE1 != null){ throw new RuntimeException("此類被設計爲單例模式,不允許重複創建對象, "請使用靜態方法getInstance()獲取實例對象"); } }
-
防止序列化和反序列化破壞單例模式
在單例類中增加readResolve()方法,可以避免實例重複。
private Object readResolve(){ return INSTANCE; }
總結
單例實現方式各有優缺點,根據業務需求斟酌選擇合適的創建方式:
優先推薦使用內部靜態類方式創建,如果需要攜帶參數,可考慮使用懶漢式+DCL雙重鎖檢驗方式創建。
工廠方法模式
定義
定義一個創建產品的對象的工廠接口,將產品對象的實際創建推遲到具體的子工廠類中,着滿足創建型模式中的
創建和使用相分離
的特點。
- 產品:我們把被創建的對象稱作
產品
。 - 工廠:把創建產品的對象稱作
工廠
。
特點
優點
- 用戶只需要知道工廠的名稱就可得到所需的產品,無需知道產品的具體創建過程;
- 在系統增加新的產品時只需要添加具體產品類和對應的具體工廠類,無需對原工廠進行任何修改,滿足
開閉原則
缺點
- 每增加一個產品就要增加一個具體產品類和對應的具體工廠類,增加了系統複雜度
結構與實現
工廠方法模式由抽象工廠
、具體工廠
、抽象產品
、具體產品
等4個要素構成。
構成
工廠方法模式主要角色如下:
-
抽象工廠(Abstract Factory):提供了創建產品的接口,調用者可通過訪問具體工廠的工廠方法來創建產品。
-
具體工廠(Concrete Factory):主要實現了抽象工廠中的抽象方法,完成具體的產品創建。
-
抽象產品(Abstract Product):定義了產品規範,描述產品的主要特徵和功能。
-
具體產品(Concrete Product):主要實現了抽象產品定義的接口,由具體的工廠來創建,同具體工廠一一對應。
實現
//1、定義汽車抽象工廠(提供了汽車生產的接口)
public interface ICarFactory {
ICar createCar();
}
//2、定義汽車抽象產品(描述汽車特性和功能)
public interface ICar {
//顏色
String color();
//類型
String type();
//速度
String speed();
void show();
}
//3、定義具體汽車產品(實現汽車抽象接口,由對應的具體汽車工廠生產)
//公共汽車
public class Bus implements ICar {
@Override
public String color() {return "藍色";}
@Override
public String type() {return "公共汽車";}
@Override
public String speed() {return "速度40邁的";}
@Override
public void show() {
System.out.println( speed() + color() + type());
}
}
//出租車
public class Taxi implements ICar {
@Override
public String color() { return "紅色";}
@Override
public String type() {return "出租車";}
@Override
public String speed() {return "速度90邁的";}
@Override
public void show() {
System.out.println( speed() + color() + type());
}
}
//校車
public class SchoolBus implements ICar {
@Override
public String color() {return "黃色";}
@Override
public String type() {return "校車";}
@Override
public String speed() {return "速度60邁的";}
@Override
public void show() {
System.out.println( speed() + color() + type());
}
}
//4、定義具體汽車工廠(實現抽象汽車工廠中的抽象方法,完成具體汽車的產品的生產)
//公共汽車工廠
public class BusFactory implements ICarFactory {
@Override
public ICar createCar() {
System.out.println("我是公共汽車工廠生產——>");
return new Bus();
}
}
//出租車工廠
public class TaxiFactory implements ICarFactory {
@Override
public ICar createCar() {
System.out.println("我是出租車工廠生產——>");
return new Taxi();
}
}
//校車工廠
public class SchoolBusFactory implements ICarFactory {
@Override
public ICar createCar() {
System.out.println("我是校車工廠生產——>");
return new SchoolBus();
}
}
//測試
public class Test {
public static void main(String[] args) {
ICarFactory busFactory = new BusFactory();
busFactory.createCar().show();
ICarFactory taxiFactory = new TaxiFactory();
taxiFactory.createCar().show();
ICarFactory schoolBusFactory = new SchoolBusFactory();
schoolBusFactory.createCar().show();
}
}
//輸出
我是公共汽車工廠生產——>速度40邁的藍色公共汽車
我是出租車工廠生產——>速度90邁的紅色出租車
我是校車工廠生產——>速度60邁的黃色校車
應用場景
工廠方法模式通常適用於以下場景
- 客戶只知道創建產品的工廠名,而不知道具體的產品名。如TCL電視工廠,海信電視工廠等;
- 創建產品的任務由多個具體子工廠中的某一個完成,而抽象工廠只提供創建產品的接口;
- 客戶不關心創建產品的細節,只關心產品的品牌。
擴展(簡單工廠模式/靜態方法模式)
當需要生成的產品不多且不會增加,一個具體的工廠類就可以完成任務時,可以刪除抽象工廠類。這時工廠方法模式
將退化到簡單工廠模式
定義
簡單工廠模式又叫靜態方法模式(因爲工廠定義了一個靜態方法)
工廠類通過創建的靜態方法根據傳入的不同參數從而創建不同的具體產品類的實例
注意:僅適合產品不多且不會增加情況,如果不滿足上述條件,增加新產品時候要修改簡單工廠類則會違背開閉原則
實現
//汽車工廠(簡單工廠模式,可以生產多個不同的具體汽車)
public class SimpleCarFactory {
public static ICar createCar(String name){
if(name.equals("Bus")){
//生產公共汽車
return new Bus();
}else if(name.equals("Taxi")){
//生產出租車
return new Taxi();
}else if(name.equals("SchoolBus")){
//生產校車
return new SchoolBus();
}
return null;
}
}
//測試
public class Test {
public static void main(String[] args) {
//簡單工廠模式
ICar bus = SimpleCarFactory.createCar("Bus");
bus.show();
ICar taxi = SimpleCarFactory.createCar("Taxi");
taxi.show();
ICar schoolBus = SimpleCarFactory.createCar("SchoolBus");
schoolBus.show();
}
}
//輸出
速度40邁的藍色公共汽車
速度90邁的紅色出租車
速度60邁的黃色校車
抽象工廠模式
概念解釋:
- 等級:相同種類稱爲同等級(比如:
海爾電視|TCL電視
、海爾冰箱|TCL冰箱
),以下稱等級。 - 產品族:將同一具體工廠生產位於不同等級的一組產品稱爲
產品族
上面的工廠方法模式
中只考慮生產同等級的產品,但現實生活中許多工廠時綜合型的,能生產多等級(種類)產品,如農場裏既養動物又養植物,電器廠既生產電視又生產洗衣機或空調,大學既有軟件專業又有生物專業等。
本節介紹抽象工廠模式
將考慮多等級(種類)產品的生產。
定義
抽象工廠模式:是一種爲訪問類提供一個創建一組相關或相互依賴對象的接口,且訪問類無需指定所要產品的具體類就能得到同族的不同等級的產品模式的結構。
抽象工廠模式是工廠方法模式的升級版本,工廠方法模式只生產一個等級的產品,而抽象工廠模式可以生產多個等級的產品。
抽象工廠模式一般要滿足以下條件:
- 系統中有多個產品族,每個具體工廠創建同一族但屬於不同等級的產品;
- 系統一次只可能消費其中某一族產品,即同族的產品一起使用。
特點
優點
-
可以在類的內部對產品族中的相關聯的多等級產品共同管理,而不必專門引入多個新的類來進行管理。
-
當增加一個新的產品族時,不需要修改源代碼,滿足開閉原則
例如下文實現中增加一個南京的牧場不需要更改原代碼即可實現
缺點
-
當產品族中要增加一個新的產品時,所有工廠類都要進行修改。
例如下文實現代碼中北京牧場在動物、植物產品基礎上需要增加一個真菌,涉及到的抽象工廠以及具體工廠都要改動
結構與實現
抽象工廠模式同工廠方法模式一樣,也是由抽象工廠
、具體工廠
、抽象產品
、具體產品
等4個要素構成,但抽象工廠中的方法個數不同,抽象產品的個數也不相同
結構
抽象工廠模式的主要角色如下:
- 抽象工廠(Abstract Factory):提供創建產品的接口,不同於
工廠方法模式
的是包含多個創建產品的方法,可以創建多個不同等級的產品。 - 具體工廠(Concrete Factory):主要實現抽象工廠中的多個抽象方法,完成具體產品的創建。
- 抽象產品(Abstract Product):定義了產品規範,描述產品的主要特徵和功能,抽象工廠模式有多個抽象產品
- 具體產品(Concrete Product):主要實現了抽象產品中定義的接口,由具體工廠創建,它同具體工廠之間是一對多關係。
實現
/**
*1、定義抽象工廠,提供創建產品的接口(農場-包含多個創建產品的方法)
*/
public interface IFarmFactory {
//抽象工廠定義創建植物的接口
IAnimal createAnimalProduct();
//抽象工廠定義創建植物的接口
IPlant createPlant();
}
/**
*2、定義抽象產品,描述特性和功能(多個抽象產品)
*/
//動物
public interface IAnimal {
//名稱
String name();
//食物
String food();
//數量
int count();
void show();
}
//植物
public interface IPlant {
//名稱
String name();
//數量
int count();
//作用
String effect();
void show();
}
/**
*3、定義具體產品,實現抽象產品中的接口,由具體工廠創建
*/
//動物具體產品-羊
public class SheepProduct implements IAnimal {
@Override
public String name() {return "羊";}
@Override
public String food() {return "只吃草的";}
@Override
public int count() {return 50;}
@Override
public void show() {
System.out.println(count() + food() + name());
}
}
//動物具體產品-狼
public class WolfProduct implements IAnimal {
@Override
public String name() {return "狼";}
@Override
public String food() {return "只吃肉的";}
@Override
public int count() {return 20;}
@Override
public void show() {
System.out.println(count() + food() + name());
}
}
//植物具體產品-玉米
public class CornProduct implements IPlant {
@Override
public String name() {return "玉米";}
@Override
public int count() {return 100;}
@Override
public String effect() {return "株飼養動物的";}
@Override
public void show() {
System.out.println(count() + effect() + name());
}
}
//植物具體產品-太陽花
public class SunflowerProduct implements IPlant {
@Override
public String name() {return "太陽花";}
@Override
public int count() {return 200;}
@Override
public String effect() {return "株榨油的";}
@Override
public void show() {
System.out.println(count() + effect() + name());
}
}
/**
*定義具體工廠,實現抽象工廠中的多個抽象放啊,完成對具體產品的創建
*/
//上海具體工廠牧場(飼養羊和種植太陽花)
public class SHFarm implements IFarmFactory {
//創建具體的動物產品
@Override
public IAnimal createAnimalProduct() {
System.out.print("我是具體工廠(上海牧場)飼養的——>");
return new SheepProduct();
}
//創建具體的植物產品
@Override
public IPlant createPlant() {
System.out.print("我是具體工廠(上海牧場)種植的——>");
return new SunflowerProduct();
}
}
//北京具體工廠牧場(飼養狼和種植玉米)
public class BJFarm implements IFarmFactory {
//創建動物產品
@Override
public IAnimal createAnimalProduct() {
System.out.print("我是具體工廠(北京牧場)飼養的——>");
return new WolfProduct();
}
//創建植物產品
@Override
public IPlant createPlant() {
System.out.print("我是具體工廠(北京牧場)種植的——>");
return new CornProduct();
}
}
//測試
public class Test {
public static void main(String[] args) {
IFarmFactory shFarm = new SHFarm();
shFarm.createAnimalProduct().show();
shFarm.createPlant().show();
IFarmFactory bjFarm = new BJFarm();
bjFarm.createAnimalProduct().show();
bjFarm.createPlant().show();
}
}
//輸出
我是具體工廠(上海牧場)飼養的——>50只吃草的羊
我是具體工廠(上海牧場)種植的——>200株榨油的太陽花
我是具體工廠(北京牧場)飼養的——>20只吃肉的狼
我是具體工廠(北京牧場)種植的——>100株飼養動物的玉米
應用場景
抽象工廠模式適用於以下場景
- 當需要創建的對象是一系列相互關聯或相互依賴的產品族時,如電器工廠中的電視機、洗衣機、空調;
- 系統中有多個產品族,但是每次只使用其中的某一族產品。如有人只喜歡穿某一個品牌的衣服和鞋子;
- 系統中提供了產品類庫,且所有的產品的接口相同,客戶端不依賴產品實例的創建細節和內部結構;
擴展
抽象工廠模式的擴展有一定的開閉原則
傾斜性:
-
當增加一個新的產品族時候只需要增加一個新的具體工廠即可,不需要需改源代碼,滿足開閉原則
例如上述代碼增加一個南京牧場產品族,不需要修改原來代碼即可實現
-
當產品族中需要增加一個新的產品時候,則所有的工廠類都需要進行修改,不滿足開閉原則
例如上述代碼北京牧場中增加一個真菌,則抽象工廠需要提供生產真菌的抽象方法,以及具體工廠需要實現生產真菌的方法。
另一方面,當系統中只存在一個等級結構的產品時候,抽象工廠模式將退化到工廠方法模式。