這又是一個(gè)新的系列啦忙迁,探究各大設(shè)計(jì)模式在開(kāi)發(fā)中必須注意思考的一些問(wèn)題,以及它們的多向使用。
文章結(jié)構(gòu):(1)單例模式概念以及優(yōu)缺點(diǎn)(2)各式各樣的單例及其線程安全問(wèn)題溃蔫。(3)使用推薦。
單例模式概念以及優(yōu)缺點(diǎn):
(1)定義:
要求一個(gè)類只能生成一個(gè)對(duì)象琳猫,所有對(duì)象對(duì)它的依賴相同伟叛。
(2)優(yōu)點(diǎn):
1. 只有一個(gè)實(shí)例,減少內(nèi)存開(kāi)支脐嫂。應(yīng)用在一個(gè)經(jīng)常被訪問(wèn)的對(duì)象上
2. 減少系統(tǒng)的性能開(kāi)銷痪伦,應(yīng)用啟動(dòng)時(shí),直接產(chǎn)生一單例對(duì)象雹锣,用永久駐留內(nèi)存的方式网沾。
3.避免對(duì)資源的多重占用
4.可在系統(tǒng)設(shè)置全局的訪問(wèn)點(diǎn),優(yōu)化和共享資源訪問(wèn)蕊爵。
(3)缺點(diǎn):
1.一般沒(méi)有接口辉哥,擴(kuò)展困難。原因:接口對(duì)單例模式?jīng)]有任何意義攒射;要求“自行實(shí)例化”醋旦,并提供單一實(shí)例,接口或抽象類不可能被實(shí)例化会放。(當(dāng)然饲齐,單例模式可以實(shí)現(xiàn)接口、被繼承咧最,但需要根據(jù)系統(tǒng)開(kāi)發(fā)環(huán)境判斷)
2.單例模式對(duì)測(cè)試是不利的捂人。如果單例模式?jīng)]完成,是不能進(jìn)行測(cè)試的矢沿。
3.單例模式與單一職責(zé)原則有沖突滥搭。原因:一個(gè)類應(yīng)該只實(shí)現(xiàn)一個(gè)邏輯,而不關(guān)心它是否是單例捣鲸,是不是要單例取決于環(huán)境瑟匆;單例模式把“要單例”和業(yè)務(wù)邏輯融合在一個(gè)類。
(4)使用場(chǎng)景:
1.要求生成唯一序列化的環(huán)境
2.項(xiàng)目需要的一個(gè)共享訪問(wèn)點(diǎn)或共享的數(shù)據(jù)點(diǎn)
3.創(chuàng)建一個(gè)對(duì)象需要消耗資源過(guò)多的情況栽惶。如:要訪問(wèn)IO和 數(shù)據(jù)庫(kù)等資源愁溜。
4.需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境⊥獬В可以采用單例模式或者直接聲明static的方式冕象。
(5)注意事項(xiàng):
1.類中其他方法,盡量是static
2.注意JVM的垃圾回收機(jī)制酣衷。
如果一個(gè)單例對(duì)象在內(nèi)存長(zhǎng)久不使用交惯,JVM就認(rèn)為對(duì)象是一個(gè)垃圾次泽。所以如果針對(duì)一些狀態(tài)值穿仪,如果回收的話席爽,應(yīng)用就會(huì)出現(xiàn)故障。
3.采用單例模式來(lái)記錄狀態(tài)值的類的兩大方法:
(一)啊片、由容器管理單例的生命周期只锻。Java EE容器或者框架級(jí)容器,自行管理對(duì)象的生命周期紫谷。
(二)狀態(tài)隨時(shí)記錄齐饮。異步記錄的方式或者使用觀察者模式,記錄狀態(tài)變化笤昨,確保重新初始化也可從資源環(huán)境獲得銷毀前的數(shù)據(jù)祖驱。
二、各式各樣的單例及其線程安全問(wèn)題:
(1)懶漢式單例:
意思:就是需要使用這個(gè)對(duì)象的時(shí)候才去創(chuàng)建這個(gè)對(duì)象瞒窒。
//懶漢式單例
public class Singleton1 {
private static Singleton1 singleton1=null;
public Singleton1(){
}
public static Singleton1 getInstance(){
if (singleton1==null){
try {
Thread.sleep(200);//我們知道初始化一個(gè)對(duì)象需要一定時(shí)間的嘛捺僻,我們用sleep假設(shè)這個(gè)時(shí)間
singleton1 = new Singleton1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return singleton1;
}
}
//測(cè)試線程
public class SingleThread1 extends Thread {
//哈希值對(duì)應(yīng)的是唯一的嘛,如果不一樣了崇裁,就說(shuō)明使用的不是同一個(gè)對(duì)象咯匕坯。
@Override
public void run() {
System.out.println(Singleton1.getInstance().hashCode());
}
}
//測(cè)試類
public class SingletonTest {
public static void main(String []args){
SingleThread1[] thread1s = new SingleThread1[10];
for (int i= 0;i<thread1s.length;i++){
thread1s[i] = new SingleThread1();
}
for (int j = 0; j < thread1s.length; j++) {
thread1s[j].start();
}
}
}
//打印的結(jié)果:
569219718
1259146238
565373737
732830316
679555294
1886445805
1557403724
635681435
622018771
1439317371
線程安全的懶漢式單例設(shè)計(jì):
1.鎖住獲取方法方式:
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3(){}
//鎖住獲取方法的方式
public synchronized static Singleton3 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
instance = new Singleton3();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
2.鎖住部分代碼塊的方式:
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance() {
try {
//鎖住代碼塊的方式
synchronized (Singleton2.class) {
if(instance != null){
}else{
Thread.sleep(200);
instance = new Singleton2();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
3.鎖住初始化對(duì)象操作的方式:但是!0挝取葛峻!這不是線程安全的!巴比!一會(huì)有這個(gè)方式的優(yōu)化從而實(shí)現(xiàn)線程安全术奖。
為什么?轻绞?
因?yàn)槎鄠€(gè)訪問(wèn)已經(jīng)進(jìn)入到創(chuàng)建的那里了腰耙。
public class Singleton4 {
private static Singleton4 instance = null;
private Singleton4(){}
public static Singleton4 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
//只鎖住初始化操作的方式
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
4.鎖住初始化對(duì)象操作的方式,但有個(gè)再檢查操作:
public class Singleton5 {
//使用volatile關(guān)鍵字保其可見(jiàn)性
volatile private static Singleton5 instance = null;
private Singleton5(){}
public static Singleton5 getInstance() {
try {
if(instance != null){
}else{
Thread.sleep(300);
//鎖住初始化操作的方式
synchronized (Singleton5.class) {
if(instance == null){//二次檢查
instance = new Singleton5();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return instance;
}
}
使用了volatile關(guān)鍵字來(lái)保證其線程間的可見(jiàn)性铲球;在同步代碼塊中使用二次檢查挺庞,以保證其不被重復(fù)實(shí)例化。集合其二者稼病,這種實(shí)現(xiàn)方式既保證了其高效性选侨,也保證了其線程安全性。
解析volatile在此的作用:
volatile(涉及java內(nèi)存模型的知識(shí))會(huì)禁止CPU對(duì)內(nèi)存訪問(wèn)重排序(并不一定禁止指令重排)然走,也就是CPU執(zhí)行初始化操作援制,那么他會(huì)保證其他CPU看到的操作順序是1.給 instance 分配內(nèi)存--2.調(diào)用 Singleton 的構(gòu)造函數(shù)來(lái)初始化成員變量--3.將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了),(雖然在CPU內(nèi)由于流水線多發(fā)射并不一定是這個(gè)順序)
不使用volatile的問(wèn)題是什么呢芍瑞?晨仑?
在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說(shuō)上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2洪己。如果是后者妥凳,則在 3 執(zhí)行完畢、2 未執(zhí)行之前答捕,被線程二搶占了逝钥,這時(shí) instance 已經(jīng)是非 null 了(但卻沒(méi)有初始化),所以線程二會(huì)直接返回 instance拱镐,然后使用艘款,然后順理成章地報(bào)錯(cuò)。
用volatile的意義并不在于其他線程一定要去內(nèi)存總讀取instance沃琅,而在于它限制了CPU對(duì)內(nèi)存操作的重拍序哗咆,使其他線程在看到3之前2一定是執(zhí)行過(guò)的。
(2)餓漢式單例:
意思是:類裝載時(shí)就實(shí)例化該單例類
public class Singleton6 {
//一初始化類就初始化這個(gè)單例了R婷肌T兰稀!
private static Singleton6 singleton6= new Singleton6();
private Singleton6(){
}
public static Singleton6 getInstance(){
return singleton6;
}
}
基于classloder機(jī)制避免了多線程的同步問(wèn)題呜叫,不過(guò)空繁,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種朱庆,在單例模式中大多數(shù)都是調(diào)用getInstance方法盛泡, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒(méi)有達(dá)到lazy loading的效果娱颊。這個(gè)是沒(méi)有懶加載的功能的0了小!箱硕!
餓漢式單例變種:
public class Singleton7 {
private static Singleton7 instance = null;
static {
instance = new Singleton7();
}
private Singleton7() {
}
public static Singleton7 getInstance() {
return instance;
}
}
(3)靜態(tài)內(nèi)部類實(shí)現(xiàn)懶加載:
//靜態(tài)內(nèi)部類單例
public class Singleton8 {
private static class SingletonHolder {
private static final Singleton8 INSTANCE = new Singleton8();
}
private Singleton8 (){}
public static final Singleton8 getInstance() {
return SingletonHolder.INSTANCE;
}
}
同樣利用了classloder的機(jī)制來(lái)保證初始化instance時(shí)只有一個(gè)線程拴竹,它跟餓漢式的兩種方式不同的是:餓漢式的兩種方式是只要Singleton類被裝載了,那么instance就會(huì)被實(shí)例化(沒(méi)有達(dá)到lazy loading效果)剧罩,而這種方式是Singleton類被裝載了栓拜,instance還未被初始化。因?yàn)镾ingletonHolder類沒(méi)有被主動(dòng)使用惠昔,只有顯示通過(guò)調(diào)用getInstance方法時(shí)幕与,才會(huì)顯示裝載SingletonHolder類,從而實(shí)例化instance镇防。想象一下啦鸣,如果實(shí)例化instance很消耗資源,我想讓他延遲加載来氧,另外一方面诫给,我不希望在Singleton類加載時(shí)就實(shí)例化香拉,因?yàn)槲也荒艽_保Singleton類還可能在其他的地方被主動(dòng)使用從而被加載,那么這個(gè)時(shí)候?qū)嵗痠nstance顯然是不合適的中狂。
靜態(tài)內(nèi)部類方式單例再度研究:序列化和反序列化問(wèn)題:
public class MySingleton implements Serializable {
private static final long serialVersionUID = 1L;
//內(nèi)部類
private static class MySingletonHandler{
private static MySingleton instance = new MySingleton();
}
private MySingleton(){}
public static MySingleton getInstance() {
return MySingletonHandler.instance;
}
}
public class SaveAndReadForSingleton {
public static void main(String[] args) {
MySingleton singleton = MySingleton.getInstance();
//創(chuàng)建個(gè)文件流
File file = new File("MySingleton.txt");
//使用節(jié)點(diǎn)流凫碌,直接與文件關(guān)聯(lián)
try {
//寫(xiě)入文件
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton);
fos.close();
oos.close();
System.out.println(singleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
//讀取文件流
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
MySingleton rSingleton = (MySingleton) ois.readObject();
fis.close();
ois.close();
System.out.println(rSingleton.hashCode());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
這樣的單例測(cè)試出來(lái)時(shí),hash是不一樣的吃型,因?yàn)闆](méi)有同步到序列化與反序列化問(wèn)題证鸥。說(shuō)明反序列化后返回的對(duì)象是重新實(shí)例化的僚楞,單例被破壞了勤晚。
解決:當(dāng)JVM從內(nèi)存中反序列化地"組裝"一個(gè)新對(duì)象時(shí),就會(huì)自動(dòng)調(diào)用readResolve方法來(lái)返回我們指定好的對(duì)象,readResolve允許class在反序列化返回對(duì)象前替換泉褐、解析在流中讀出來(lái)的對(duì)象赐写。實(shí)現(xiàn)readResolve方法,一個(gè)class可以直接控制反序化返回的類型和對(duì)象引用膜赃。
public class MySingleton1 implements Serializable {
private static final long serialVersionUID = 1L;
//內(nèi)部類
private static class MySingletonHandler{
private static MySingleton1 instance = new MySingleton1();
}
private MySingleton1(){}
public static MySingleton1 getInstance() {
return MySingletonHandler.instance;
}
//該方法在反序列化時(shí)會(huì)被調(diào)用挺邀,該方法不是接口定義的方法,有點(diǎn)兒約定俗成的感覺(jué)
protected Object readResolve() throws ObjectStreamException {
System.out.println("調(diào)用了readResolve方法跳座!");
return MySingletonHandler.instance;
}
}
修改SaveAndReadForSingleton文件中的MySingleton端铛,輸出
2133927002
調(diào)用了readResolve方法!解決序列化與反序列化問(wèn)題疲眷!
2133927002
(4)枚舉:
//枚舉實(shí)現(xiàn)單例
public enum EnumSingletonFactory {
singletonFactory;
private EnumSingleton instance;
private EnumSingletonFactory(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
instance = new EnumSingleton();
}
public EnumSingleton getInstance(){
return instance;
}
}
在thread中調(diào)用實(shí)現(xiàn):
@Override
public void run() { System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
}
但是此博客 引起我思考禾蚕,是違反單一職責(zé)的,因?yàn)樗┞读嗣杜e的細(xì)節(jié)狂丝,所以我們需要改造他换淆。
//使用工廠來(lái)生成枚舉類
//通過(guò)工廠類的靜態(tài)方法去訪問(wèn)枚舉類,然后通過(guò)枚舉類訪問(wèn)它的單例几颜。
public class ClassFactory {
private enum MyEnumSingleton{
singletonFactory;
private EnumSingleton instance;
private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實(shí)例化
instance = new EnumSingleton();
}
public EnumSingleton getInstance(){
return instance;
}
}
public static EnumSingleton getInstance(){
return MyEnumSingleton.singletonFactory.getInstance();
}
}
在thread中調(diào)用實(shí)現(xiàn):
@Override
public void run() {
System.out.println(ClassFactory.getInstance().hashCode());
}
枚舉類的方式不僅能避免多線程同步問(wèn)題倍试,而且還能防止反序列化重新創(chuàng)建新的對(duì)象。不過(guò)實(shí)際工程代碼中蛋哭,很少去用此方式县习。
三、推薦使用:
上述的各種單例都講完了:基本是五種寫(xiě)法谆趾。懶漢准颓,惡漢,雙重校驗(yàn)鎖棺妓,枚舉和靜態(tài)內(nèi)部類攘已。
(1)餓漢式單例。
原因:類的加載機(jī)制保證了怜跑,類初始化時(shí)样勃,只執(zhí)行一次靜態(tài)代碼塊以及類變量初始化吠勘。直接保證了唯一性,保證了線程安全峡眶。(一般使用非靜態(tài)代碼塊方式)
(2)靜態(tài)內(nèi)部類方式:
原因:懶加載唄>绶馈!辫樱!應(yīng)用在一些十分巨大的單例bean中峭拘。
參考博客:此博客讓我對(duì)單例加深了一大層,感謝感謝Jㄊ睢鸡挠!
好了,設(shè)計(jì)模式(一)--深入單例模式(涉及線程安全問(wèn)題)講完了搬男。本博客是我復(fù)習(xí)階段的一些筆記拣展,拿來(lái)分享經(jīng)驗(yàn)給大家。歡迎在下面指出錯(cuò)誤缔逛,共同學(xué)習(xí)1赴!!你的點(diǎn)贊是對(duì)我最好的支持:峙按脚!