一肄渗、寫在前面
在Java眾多的設(shè)計(jì)模式當(dāng)中后雷,單例模式算是用的最多的設(shè)計(jì)模式之一了季惯,接下來(lái)我將介紹一下市面上常用的幾種寫法以及各自的優(yōu)缺點(diǎn)分析,廢話不多說(shuō)直接進(jìn)入正題臀突。
二勉抓、單例模式常見寫法
眾所周知“單例”顧名思義就是一個(gè)類只能有一個(gè)實(shí)例,就好比一個(gè)國(guó)家只能有一個(gè)政府一樣候学,單例模式在寫法上基本都有一個(gè)共同點(diǎn):通過私有化構(gòu)造器從而屏蔽掉外部類創(chuàng)建實(shí)例的可能(排除通過反射和反序列化操作)藕筋,下面我就通過政府Government這個(gè)類來(lái)實(shí)現(xiàn)幾種不同的單例模式。
2.1 餓漢式
餓漢式即無(wú)論程序后面用不用的到這個(gè)實(shí)例梳码,它都會(huì)在加載該類的時(shí)候就創(chuàng)建一個(gè)實(shí)例隐圾。然后通過一個(gè)getInstance()的靜態(tài)方法來(lái)獲取該實(shí)例。下面是具體實(shí)現(xiàn)邏輯:
public class Government {
private static Government INSTANCE = new Government();
private Government() {
}
public static Government getInstance() {
return INSTANCE;
}
}
優(yōu)點(diǎn):
(1)餓漢式是典型的空間換時(shí)間掰茶,當(dāng)類裝載的時(shí)候就會(huì)創(chuàng)建類實(shí)例暇藏,不管你用不用,先創(chuàng)建出來(lái)濒蒋,然后每次調(diào)用的時(shí)候盐碱,就不需要再判斷了,節(jié)省了運(yùn)行時(shí)間。
(2) 餓漢式是線程安全的瓮顽,因?yàn)樘摂M機(jī)保證只會(huì)裝載一次县好,在裝載類的時(shí)候是不會(huì)發(fā)生并發(fā)的。
缺點(diǎn):
(1)在沒用到的時(shí)候就創(chuàng)建出實(shí)例趣倾,導(dǎo)致浪費(fèi)內(nèi)存空間聘惦。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題。例如以下測(cè)試代碼你就會(huì)發(fā)現(xiàn)該問題儒恋。(Government類需要實(shí)現(xiàn)Serializable接口)
public static void main(String[] args) {
Government instance1 = Government.getInstance();
Government instance2 = Government.getInstance();
System.out.println(instance1);
System.out.println(instance2);
try {
//通過反射獲取實(shí)例3
Class<Government> governmentClass1 = (Class<Government>) Class.forName("com.example.singlemode.Government");
Constructor<Government> constructor1 = governmentClass1.getDeclaredConstructor(null);
constructor1.setAccessible(true);
Government instance3 = constructor1.newInstance();
//通過反射獲取實(shí)例4
Class<Government> governmentClass2 = (Class<Government>) Class.forName("com.example.singlemode.Government");
Constructor<Government> constructor2 = governmentClass2.getDeclaredConstructor(null);
constructor2.setAccessible(true);
Government instance4 = constructor2.newInstance();
System.out.println(instance3);
System.out.println(instance4);
//通過反序列化獲取實(shí)例對(duì)象
//1.將instance1寫入到磁盤
FileOutputStream fos = new FileOutputStream("object.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance1);
oos.close();
// 2. 把硬盤文件上的對(duì)象讀出來(lái)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out"));
Government instance5 = (Government) ois.readObject();
ois.close();
System.out.println(instance5);
} catch (Exception e) {
e.printStackTrace();
}
}
執(zhí)行結(jié)果:
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@60e53b93
com.example.singlemode.Government@5e2de80c
com.example.singlemode.Government@6f496d9f
你會(huì)發(fā)現(xiàn)1善绎,2通過getInstance()方法獲取的實(shí)例相同,3诫尽,4通過反射獲取到的實(shí)例不同禀酱,這就是反射導(dǎo)致的獲取多個(gè)實(shí)例的問題,5通過將1的實(shí)例寫入磁盤在反序列化獲取的實(shí)例和1之前的也不同牧嫉,這就是反序列化導(dǎo)致的獲取到不同實(shí)例的問題剂跟。
針對(duì)這兩個(gè)問題可以采取以下方式避免,代碼如下:
public class Government implements Serializable{
private static Government INSTANCE = new Government();
private Government() {
//解決反射問題酣藻,一旦通過反射調(diào)用構(gòu)造器創(chuàng)建對(duì)象后就拋出異常
if (null != INSTANCE) {
throw new RuntimeException();
}
}
public static Government getInstance() {
return INSTANCE;
}
// 防止反序列化獲取多個(gè)對(duì)象的漏洞曹洽。
// 無(wú)論是實(shí)現(xiàn)Serializable接口,或是Externalizable接口辽剧,當(dāng)從I/O流中讀取對(duì)象時(shí)送淆,readResolve()方法都會(huì)被調(diào)用到。
// 實(shí)際上就是用readResolve()中返回的對(duì)象直接替換在反序列化過程中創(chuàng)建的對(duì)象怕轿。
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
再次運(yùn)行之前的代碼會(huì)發(fā)現(xiàn)執(zhí)行到反射獲取對(duì)象的地方時(shí)候回拋出異常:
先將反射獲取對(duì)象的代碼注釋掉偷崩,然后看反序列化運(yùn)行的結(jié)果:
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0
最后一個(gè)是反序列化的,發(fā)現(xiàn)和之前的是同一個(gè)實(shí)例撞羽。這就說(shuō)明解決了這兩個(gè)問題阐斜。
2.2 懶漢式
和餓漢式不同之處就在于屎鳍,它是采用懶加載在用到的時(shí)候才會(huì)去創(chuàng)建,代碼實(shí)現(xiàn)如下:
public class Government {
private static Government INSTANCE;
private Government() {
}
public static Government getInstance() {
if (INSTANCE == null) {
INSTANCE = new Government();
}
return INSTANCE;
}
}
優(yōu)點(diǎn):
(1) 如果一直沒有使用的話萨蚕,那就不會(huì)創(chuàng)建實(shí)例试幽,節(jié)約內(nèi)存空間
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷综苔,看是否需要?jiǎng)?chuàng)建實(shí)例扛伍,浪費(fèi)判斷的時(shí)間稚伍。
(2)從線程安全性上講床三,不加同步的懶漢式是線程不安全的酥泛,比如惕澎,有兩個(gè)線程莉测,一個(gè)是線程A,一個(gè)是線程B唧喉,它們同時(shí)調(diào)用getInstance方法捣卤,那就可能導(dǎo)致并發(fā)問題忍抽。
(3)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題。解決方式和之前餓漢式的相似這里不做過多說(shuō)明董朝。
2.3 DoubleCheck式
DoubleCheck式也叫雙重檢查式鸠项,主要是進(jìn)行了兩重判空操作,同時(shí)增加了同步鎖具體實(shí)現(xiàn)如下:
public class Government implements Serializable{
private volatile static Government INSTANCE;
private Government() {
}
public static Government getInstance() {
if (INSTANCE == null) {
synchronized (Government.class){
if (INSTANCE==null){
INSTANCE = new Government();
}
}
}
return INSTANCE;
}
}
優(yōu)點(diǎn):
(1) 一直沒有使用的話,那就不會(huì)創(chuàng)建實(shí)例子姜,節(jié)約內(nèi)存空間
(2)加了同步鎖祟绊,線程安全
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷,第一次會(huì)持有同步鎖,浪費(fèi)判斷的時(shí)間和性能損耗哥捕。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題牧抽。解決方式和之前餓漢式的相似這里不做過多說(shuō)明。
2.4 靜態(tài)內(nèi)部類式
靜態(tài)內(nèi)部類實(shí)現(xiàn)方式(也是一種懶加載方式)遥赚,具體實(shí)現(xiàn)如下:
public class Government implements Serializable{
private static class GovernmentHolder{
private static final Government INSTANCE = new Government();
}
private Government() {
}
public static Government getInstance() {
return GovernmentHolder.INSTANCE;
}
}
優(yōu)點(diǎn):
(1) 一直沒有使用的話扬舒,那就不會(huì)創(chuàng)建實(shí)例,節(jié)約內(nèi)存空間
(2)采用靜態(tài)內(nèi)部類凫佛,線程安全
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷和持有同步鎖讲坎,浪費(fèi)判斷的時(shí)間和性能損耗。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題愧薛。解決方式和之前餓漢式的相似這里不做過多說(shuō)明晨炕。
2.5 枚舉式
枚舉式絕對(duì)是這五種里堪稱最完美的模式,簡(jiǎn)簡(jiǎn)單單的一點(diǎn)代碼就實(shí)現(xiàn)了一個(gè)線程安全毫炉,lazy loading的單例瓮栗,與其說(shuō)是寫法鬼斧神工,不如說(shuō)是恰如其分地應(yīng)用了enum的性質(zhì)碘箍。
public enum Government {
INSTANCE
}
首先遵馆,我們都知道enum是由class實(shí)現(xiàn)的鲸郊,換言之丰榴,enum可以實(shí)現(xiàn)很多class的內(nèi)容,包括可以有member和member function秆撮,這也是我們可以用enum作為一個(gè)類來(lái)實(shí)現(xiàn)單例的基礎(chǔ)四濒。另外,由于enum是通過繼承了Enum類實(shí)現(xiàn)的职辨,enum結(jié)構(gòu)不能夠作為子類繼承其他類盗蟆,但是可以用來(lái)實(shí)現(xiàn)接口。此外舒裤,enum類也不能夠被繼承喳资,在反編譯中,我們會(huì)發(fā)現(xiàn)該類是final的腾供。
其次仆邓,enum有且僅有private的構(gòu)造器鲜滩,防止外部的額外構(gòu)造,這恰好和單例模式吻合节值,也為保證單例性做了一個(gè)鋪墊徙硅。這里展開說(shuō)下這個(gè)private構(gòu)造器,如果我們不去手寫構(gòu)造器搞疗,則會(huì)有一個(gè)默認(rèn)的空參構(gòu)造器嗓蘑,我們也可以通過給枚舉變量參量來(lái)實(shí)現(xiàn)類的初始化,例如:
public enum Color{
RED(1),GREEN(2),BLUE(3);
private int code;
Color(int code){
this.code=code;
}
public int getCode(){
return code;
}
}
三、總結(jié)
單例模式的實(shí)現(xiàn)形式有多種匿乃,在使用的時(shí)候根據(jù)需要恰當(dāng)?shù)倪x擇實(shí)現(xiàn)形式桩皿。本文內(nèi)容算是自己對(duì)單例模式的一些認(rèn)識(shí)和理解,也是參考了一些前輩們的思路扳埂,這里向他們致謝业簿。如有錯(cuò)誤望大家指正。