Java中單例(Singleton)模式是一種廣泛使用的設(shè)計(jì)模式饮醇。單例模式的主要作用是保證在Java程序中,某個(gè)類只有一個(gè)實(shí)例存在蝶防。
一些管理器和控制器常被設(shè)計(jì)成單例模式窄锅。
單例模式好處:
它能夠避免實(shí)例對(duì)象的重復(fù)創(chuàng)建,不僅可以減少每次創(chuàng)建對(duì)象的時(shí)間開(kāi)銷木柬,還可以節(jié)約內(nèi)存空間皆串;
能夠避免由于操作多個(gè)實(shí)例導(dǎo)致的邏輯錯(cuò)誤。
如果一個(gè)對(duì)象有可能貫穿整個(gè)應(yīng)用程序眉枕,而且起到了全局統(tǒng)一管理控制的作用恶复,那么單例模式也許是一個(gè)值得考慮的選擇。
單例模式有很多種寫法速挑,大部分寫法都或多或少有一些不足谤牡。下面將分別對(duì)這幾種寫法進(jìn)行介紹。
1姥宝、餓漢模式
public class Singleton{
? ? private static Singleton instance = new Singleton();
? ? private Singleton(){}
? ? public static Singleton newInstance(){
? ? ? ? return instance;
? ? }
}
?
從代碼中我們看到翅萤,類的構(gòu)造函數(shù)定義為private的,保證其他類不能實(shí)例化此類腊满,然后提供了一個(gè)靜態(tài)實(shí)例并返回給調(diào)用者套么。餓漢模式是最簡(jiǎn)單的一種實(shí)現(xiàn)方式培己,餓漢模式在類加載的時(shí)候就對(duì)實(shí)例進(jìn)行創(chuàng)建,實(shí)例在整個(gè)程序周期都存在胚泌。
它的好處是只在類加載的時(shí)候創(chuàng)建一次實(shí)例省咨,不會(huì)存在多個(gè)線程創(chuàng)建多個(gè)實(shí)例的情況,避免了多線程同步的問(wèn)題诸迟。
它的缺點(diǎn)也很明顯茸炒,即使這個(gè)單例沒(méi)有用到也會(huì)被創(chuàng)建,而且在類加載之后就被創(chuàng)建阵苇,內(nèi)存就被浪費(fèi)了壁公。
這種實(shí)現(xiàn)方式適合單例占用內(nèi)存比較小,在初始化時(shí)就會(huì)被用到的情況绅项。但是紊册,如果單例占用的內(nèi)存比較大,或單例只是在某個(gè)特定場(chǎng)景下才會(huì)用到快耿,使用餓漢模式就不合適了囊陡,這時(shí)候就需要用到懶漢模式進(jìn)行延遲加載。
2掀亥、懶漢模式
public class Singleton{
? ? private static Singleton instance = null;
? ? private Singleton(){}
? ? public static Singleton newInstance(){
? ? ? ? if(null == instance){
? ? ? ? ? ? instance = new Singleton();
? ? ? ? }
? ? ? ? return instance;
? ? }
}
好處:懶漢模式中單例是在需要的時(shí)候才去創(chuàng)建的撞反,如果單例已經(jīng)創(chuàng)建,再次調(diào)用獲取接口將不會(huì)重新創(chuàng)建新的對(duì)象搪花,而是直接返回之前創(chuàng)建的對(duì)象遏片。
適用于:如果某個(gè)單例使用的次數(shù)少,并且創(chuàng)建單例消耗的資源較多撮竿,那么就需要實(shí)現(xiàn)單例的按需創(chuàng)建吮便,這個(gè)時(shí)候使用懶漢模式就是一個(gè)不錯(cuò)的選擇。
缺點(diǎn):但是這里的懶漢模式并沒(méi)有考慮線程安全問(wèn)題幢踏,在多個(gè)線程可能會(huì)并發(fā)調(diào)用它的getInstance()方法髓需,導(dǎo)致創(chuàng)建多個(gè)實(shí)例,因此需要加鎖解決線程同步問(wèn)題房蝉,實(shí)現(xiàn)如下:
public class Singleton{
? ? private static Singleton instance = null;
? ? private Singleton(){}
? ? public static synchronized Singleton newInstance(){
? ? ? ? if(null == instance){? // Single Checked
? ? ? ? ? ? instance = new Singleton();
? ? ? ? }
? ? ? ? return instance;
? ? }
}
3僚匆、雙重校驗(yàn)鎖【推薦】
這個(gè)問(wèn)題在Java面試中經(jīng)常被問(wèn)到,但是面試官對(duì)回答此問(wèn)題的滿意度僅為50%搭幻。
一半的人寫不出雙檢鎖白热,還有一半的人說(shuō)不出它的隱患和Java1.5是如何對(duì)它修正的。
它其實(shí)是一個(gè)用來(lái)創(chuàng)建線程安全的單例的老方法粗卜,當(dāng)單例實(shí)例第一次被創(chuàng)建時(shí)它試圖用單個(gè)鎖進(jìn)行性能優(yōu)化,
但是由于太過(guò)于復(fù)雜在JDK1.4中它是失敗的纳击,我個(gè)人也不喜歡它续扔。無(wú)論如何攻臀,即便你也不喜歡它但是還是要了解一下,因?yàn)樗?jīng)常被問(wèn)到纱昧。
加鎖的懶漢模式看起來(lái)即解決了線程并發(fā)問(wèn)題刨啸,又實(shí)現(xiàn)了延遲加載,然而它存在著性能問(wèn)題识脆,依然不夠完美设联。
synchronized修飾的同步方法比一般方法要慢很多,如果多次調(diào)用getInstance()灼捂,累積的性能損耗就比較大了离例。
因此就有了雙重校驗(yàn)鎖,先看下它的實(shí)現(xiàn)代碼悉稠。
public class Singleton {
? ? private static Singleton instance = null;
? ? private Singleton(){}
? ? public static Singleton getInstance() {
? ? ? ? if (instance == null) {? // Single Checked
? ? ? ? ? ? synchronized (Singleton.class) {
? ? ? ? ? ? ? ? if (instance == null) { // Double checked
? ? ? ? ? ? ? ? ? ? instance = new Singleton();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return instance;
? ? }
}
可以看到上面在同步代碼塊外多了一層instance為空的判斷宫蛆。由于單例對(duì)象只需要?jiǎng)?chuàng)建一次,如果后面再次調(diào)用getInstance()只需要直接返回單例對(duì)象的猛。
因此耀盗,大部分情況下,調(diào)用getInstance()都不會(huì)執(zhí)行到同步代碼塊卦尊,從而提高了程序性能叛拷。
不過(guò)還需要考慮一種情況,假如兩個(gè)線程A岂却、B忿薇,A執(zhí)行了if (instance == null)語(yǔ)句,它會(huì)認(rèn)為單例對(duì)象沒(méi)有創(chuàng)建淌友,此時(shí)線程切到B也執(zhí)行了同樣的語(yǔ)句煌恢,B也認(rèn)為單例對(duì)象沒(méi)有創(chuàng)建,然后兩個(gè)線程依次執(zhí)行同步代碼塊震庭,并分別創(chuàng)建了一個(gè)單例對(duì)象瑰抵。為了解決這個(gè)問(wèn)題,還需要在同步代碼塊中增加if (instance == null)語(yǔ)句器联,也就是上面看到的代碼中的校驗(yàn)2二汛。
雙檢鎖隱患:
我們看到雙重校驗(yàn)鎖即實(shí)現(xiàn)了延遲加載,又解決了線程并發(fā)問(wèn)題拨拓,同時(shí)還解決了執(zhí)行效率問(wèn)題肴颊,是否真的就萬(wàn)無(wú)一失了呢?
這里要提到Java中的指令重排優(yōu)化渣磷。所謂指令重排優(yōu)化是指在不改變?cè)Z(yǔ)義的情況下婿着,通過(guò)調(diào)整指令的執(zhí)行順序讓程序運(yùn)行的更快。
JVM中并沒(méi)有規(guī)定編譯器優(yōu)化相關(guān)的內(nèi)容,也就是說(shuō)JVM可以自由的進(jìn)行指令重排序的優(yōu)化竟宋。
這個(gè)問(wèn)題的關(guān)鍵就在于由于指令重排優(yōu)化的存在提完,導(dǎo)致初始化Singleton和將對(duì)象地址賦給instance字段的順序是不確定的。
在某個(gè)線程創(chuàng)建單例對(duì)象時(shí)丘侠,在構(gòu)造方法被調(diào)用之前徒欣,就為該對(duì)象分配了內(nèi)存空間并將對(duì)象的字段設(shè)置為默認(rèn)值。
此時(shí)就可以將分配的內(nèi)存地址賦值給instance字段了蜗字,然而該對(duì)象可能還沒(méi)有初始化打肝。若緊接著另外一個(gè)線程來(lái)調(diào)用getInstance,取到的就是狀態(tài)不正確的對(duì)象挪捕,程序就會(huì)出錯(cuò)粗梭。
JDK5的修正:以上就是雙重校驗(yàn)鎖會(huì)失效的原因,不過(guò)還好在JDK1.5及之后版本增加了volatile關(guān)鍵字担神。
volatile的一個(gè)語(yǔ)義是禁止指令重排序優(yōu)化楼吃,也就保證了instance變量被賦值的時(shí)候?qū)ο笠呀?jīng)是初始化過(guò)的,從而避免了上面說(shuō)到的問(wèn)題妄讯。
Java中的volatile 變量是什么孩锡?
理解volatile關(guān)鍵字的作用的前提是要理解Java內(nèi)存模型,volatile關(guān)鍵字的作用主要有兩個(gè):
(1)多線程主要圍繞可見(jiàn)性和原子性兩個(gè)特性而展開(kāi)亥贸,使用volatile關(guān)鍵字修飾的變量躬窜,保證了其在多線程之間的可見(jiàn)性,
即每次讀取到volatile變量炕置,一定是最新的數(shù)據(jù)
(2)代碼底層執(zhí)行不像我們看到的高級(jí)語(yǔ)言—-Java程序這么簡(jiǎn)單荣挨,
它的執(zhí)行是Java代碼–>字節(jié)碼–>根據(jù)字節(jié)碼執(zhí)行對(duì)應(yīng)的C/C++代碼–>C/C++代碼被編譯成匯編語(yǔ)言–>和硬件電路交互,
現(xiàn)實(shí)中朴摊,為了獲取更好的性能JVM可能會(huì)對(duì)指令進(jìn)行重排序默垄,多線程下可能會(huì)出現(xiàn)一些意想不到的問(wèn)題。
使用volatile則會(huì)對(duì)禁止語(yǔ)義重排序甚纲,當(dāng)然這也一定程度上降低了代碼執(zhí)行效率
從實(shí)踐角度而言口锭,volatile的一個(gè)重要作用就是和CAS結(jié)合,保證了原子性介杆,
詳細(xì)的可以參見(jiàn)java.util.concurrent.atomic包下的類鹃操,比如AtomicInteger。
CAS(Compare and swap)比較和替換是設(shè)計(jì)并發(fā)算法時(shí)用到的一種技術(shù)春哨。
簡(jiǎn)單來(lái)說(shuō)荆隘,比較和替換是使用一個(gè)期望值和一個(gè)變量的當(dāng)前值進(jìn)行比較,如果當(dāng)前變量的值與我們期望的值相等赴背,就使用一個(gè)新值替換當(dāng)前變量的值椰拒。
volatile是一個(gè)特殊的修飾符晶渠,只有成員變量才能使用它。
在Java并發(fā)程序缺少同步類的情況下耸三,多線程對(duì)成員變量的操作對(duì)其它線程是透明的乱陡。
volatile變量可以保證下一個(gè)讀取操作會(huì)在前一個(gè)寫操作之后發(fā)生。
來(lái)源: http://blog.csdn.net/fly910905/article/details/79283557
代碼如下:
public class Singleton {
? ? private static volatile Singleton instance = null;
? ? private Singleton(){}
? ? public static Singleton getInstance() {
? ? ? ? if (instance == null) { // Single Checked
? ? ? ? ? ? synchronized (Singleton.class) {
? ? ? ? ? ? ? ? if (instance == null) { // Double checked
? ? ? ? ? ? ? ? ? ? instance = new Singleton();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return instance;
? ? }
}
4仪壮、靜態(tài)內(nèi)部類【推薦】
除了上面的三種方式,還有另外一種實(shí)現(xiàn)單例的方式胳徽,通過(guò)靜態(tài)內(nèi)部類來(lái)實(shí)現(xiàn)积锅。
首先看一下它的實(shí)現(xiàn)代碼:
public class Singleton{
? ? private static class SingletonHolder{
? ? ? ? public static Singleton instance = new Singleton();
? ? }
? ? private Singleton(){}
? ? public static Singleton newInstance(){
? ? ? ? return SingletonHolder.instance;
? ? }
}
這種方式同樣利用了類加載機(jī)制來(lái)保證只創(chuàng)建一個(gè)instance實(shí)例。它與餓漢模式一樣养盗,也是利用了類加載機(jī)制缚陷,因此不存在多線程并發(fā)的問(wèn)題。
不一樣的是往核,它是在內(nèi)部類里面去創(chuàng)建對(duì)象實(shí)例箫爷。
這樣的話,只要應(yīng)用中不使用內(nèi)部類聂儒,JVM就不會(huì)去加載這個(gè)單例類虎锚,也就不會(huì)創(chuàng)建單例對(duì)象,從而實(shí)現(xiàn)懶漢式的延遲加載衩婚。也就是說(shuō)這種方式可以同時(shí)保證延遲加載和線程安全窜护。
5、枚舉
class Resource{
}
public enum SomeThing {
? ? INSTANCE;
? ? private Resource instance;
? ? private SomeThing() {
? ? ? ? instance = new Resource();
? ? }
? ? public Resource getInstance() {
? ? ? ? return instance;
? ? }
}
上面的類Resource是我們要應(yīng)用單例模式的資源非春,具體可以表現(xiàn)為網(wǎng)絡(luò)連接柱徙,數(shù)據(jù)庫(kù)連接,線程池等等奇昙。
獲取資源的方式很簡(jiǎn)單护侮,只要 SomeThing.INSTANCE.getInstance() 即可獲得所要實(shí)例。
下面我們來(lái)看看單例是如何被保證的:
首先储耐,在枚舉中我們明確了構(gòu)造方法限制為私有羊初,在我們?cè)L問(wèn)枚舉實(shí)例時(shí)會(huì)執(zhí)行構(gòu)造方法。
同時(shí)每個(gè)枚舉實(shí)例都是static final類型的弧岳,也就表明只能被實(shí)例化一次凳忙。在調(diào)用構(gòu)造方法時(shí),我們的單例被實(shí)例化禽炬。
也就是說(shuō)涧卵,因?yàn)閑num中的實(shí)例被保證只會(huì)被實(shí)例化一次,所以我們的INSTANCE也被保證實(shí)例化一次腹尖。
可以看到柳恐,枚舉實(shí)現(xiàn)單例還是比較簡(jiǎn)單的,除此之外我們?cè)賮?lái)看一下Enum這個(gè)類的聲明:
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable
可以看到,枚舉也提供了序列化機(jī)制乐设。某些情況讼庇,比如我們要通過(guò)網(wǎng)絡(luò)傳輸一個(gè)數(shù)據(jù)庫(kù)連接的句柄,會(huì)提供很多幫助近尚。
最后借用 《Effective Java》一書中的話蠕啄,
單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
示例:
/**
* @Title: java單例之enum實(shí)現(xiàn)方式
* @ClassName: EnumSingleton.java
* @Description:
*
* @Copyright 2016-2018 - Powered By 研發(fā)中心
* @author: 王延飛
* @date:? 2018-02-07 20:02
* @version V1.0
*/
public class EnumSingleton{
? ? private EnumSingleton(){}
? ? public static EnumSingleton getInstance(){
? ? ? ? return Singleton.INSTANCE.getInstance();
? ? }
? ? private static enum Singleton{
? ? ? ? INSTANCE;
? ? ? ? private EnumSingleton singleton;
? ? ? ? //JVM會(huì)保證此方法絕對(duì)只調(diào)用一次
? ? ? ? private Singleton(){
? ? ? ? ? ? singleton = new EnumSingleton();
? ? ? ? }
? ? ? ? public EnumSingleton getInstance(){
? ? ? ? ? ? return singleton;
? ? ? ? }
? ? }
? ? public static void main(String[] args) {
? ? ? ? EnumSingleton obj1 = EnumSingleton.getInstance();
? ? ? ? EnumSingleton obj2 = EnumSingleton.getInstance();
? ? ? ? //輸出結(jié)果:obj1==obj2?true
? ? ? ? System.out.println("obj1==obj2?" + (obj1==obj2));
? ? }
}
上面提到的四種實(shí)現(xiàn)單例的方式都有共同的缺點(diǎn):
1)需要額外的工作來(lái)實(shí)現(xiàn)序列化戈锻,否則每次反序列化一個(gè)序列化的對(duì)象時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例歼跟。
2)可以使用反射強(qiáng)行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器格遭,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)哈街。
而枚舉類很好的解決了這兩個(gè)問(wèn)題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外拒迅,還提供了自動(dòng)序列化機(jī)制骚秦,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。
6璧微、單例模式的線程安全性
首先要說(shuō)的是單例模式的線程安全意味著:某個(gè)類的實(shí)例在多線程環(huán)境下只會(huì)被創(chuàng)建一次出來(lái)作箍。單例模式有很多種的寫法,我總結(jié)一下:
(1)餓漢式:線程安全
(2)懶漢式:非線程安全
(3)雙檢鎖:線程安全
(4)靜態(tài)內(nèi)部類:線程安全
(5)枚舉:線程安全
參考來(lái)源: http://www.importnew.com/12773.html
參考來(lái)源: http://blog.csdn.net/goodlixueyong/article/details/51935526