概述
單例模式是比較簡(jiǎn)單的設(shè)計(jì)模式,使用也是非常的廣泛??匆幌玛P(guān)于單例模式的定義:
確保一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例。
它的定義包含了三個(gè)要點(diǎn):
- 一個(gè)類只有一個(gè)實(shí)例:外部程序無法通過new關(guān)鍵字來創(chuàng)建實(shí)例,構(gòu)造方法用private修飾
- 自行實(shí)例化:由類本身創(chuàng)建實(shí)例對(duì)象,為了外界可以訪問這個(gè)實(shí)例,需要定義成靜態(tài)的私有成員變量。
- 提供實(shí)例: 因?yàn)闊o法提供new來床架對(duì)象,只能提供一個(gè)公有靜態(tài)方法,返回創(chuàng)建的對(duì)象實(shí)例。
來看一下單例模式的通用代碼:
餓漢式單例
class Singleton {
private static final Singleton singletonTest = new Singleton(); //創(chuàng)建實(shí)例
private Singleton() {} //構(gòu)造方法私有
public static Singleton getInstance(){
return singletonTest ; //返回實(shí)例
}
}
public class SingletonTest{
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance() ;
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); //true
}
}
上面這種寫法是最簡(jiǎn)單的寫法,但是有一個(gè)問題,就是在類加載的時(shí)候就創(chuàng)建了實(shí)例,而不管是否需要?jiǎng)?chuàng)建這個(gè)實(shí)例,沒有做到延遲加載 ,增加了負(fù)載。
懶漢式單例
為了解決上面的問題,對(duì)上面的通用代碼進(jìn)行如下改進(jìn),當(dāng)系統(tǒng)需要該實(shí)例的時(shí)候,才產(chǎn)生實(shí)例對(duì)象,并返回,代碼如下:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
singleton = new LazySingleton() ;
}
return singleton ;
}
}
通過上面的代碼發(fā)現(xiàn),在類加載的時(shí)候并沒有進(jìn)行類的初始化操作,只有在第一次調(diào)用getInstance()方法時(shí),才進(jìn)行了實(shí)例化操作,實(shí)現(xiàn)了延遲加載的功能。那么問題又來了,啥問題問題,上面的類在單線程環(huán)境是沒有問題的,但是在多線程環(huán)境中是不安全的無法保證實(shí)例的唯一性,那么在改進(jìn)一下,看下面代碼:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if(singleton == null ){
singleton = new LazySingleton() ;
}
return singleton ;
}
}
在getInstance的方法上添加了synchronized關(guān)鍵字,來保證線程之間的同步,確保實(shí)例的唯一性??雌饋硪呀?jīng)解決了上面的問題,但是新的問題其實(shí)又出現(xiàn)了,什么問題呢? 就是每次訪問getInstance()方法時(shí)都進(jìn)進(jìn)行線程鎖定的判斷,在多線程高并發(fā)的環(huán)境下,使得系統(tǒng)的性能大大的降低了,那么有什么辦法來解決這個(gè)問題嗎? 當(dāng)然有,看下面代碼:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
synchronized (LazySingleton.class){
singleton = new LazySingleton() ;
}
}
return singleton ;
}
}
最初為了保證線程之間的同步問題,給getInstance()的整個(gè)方法加了鎖,其實(shí)要保證同步的罪魁禍?zhǔn)灼鋵?shí)是 singleton=new LazySingleton()這句,只要保證了它的同步,產(chǎn)生的實(shí)例就是唯一的,所以使用synchronized塊,來保證線程之間的同步。 這樣在完成了singleton初始化之后,便不會(huì)在進(jìn)入synchronized中,系統(tǒng)的性能便不會(huì)受影響了。 程序看起來已經(jīng)很完美了,事實(shí)確實(shí)是這樣嗎? 當(dāng)然不是,再次被打臉了,分析一下,假設(shè)現(xiàn)在有兩個(gè)線程A ,B 都執(zhí)行到了 if() 出,線程A先執(zhí)行了,獲得了鎖,此時(shí)完成了singleton的初始化操作,釋放鎖 ,B開始執(zhí)行,因?yàn)锽并不知道此時(shí)singleton已經(jīng)完成了實(shí)例的創(chuàng)建,會(huì)再次創(chuàng)建一個(gè)實(shí)例。 好的,我們?cè)诟倪M(jìn)一下 :
class LazySingleton{
private volatile static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
synchronized (LazySingleton.class){
if(singleton == null){
singleton = new LazySingleton() ;
}
}
}
return singleton ;
}
}
在synchronized塊中,又進(jìn)行了一次條件判斷,這種方式稱為雙重檢查鎖定。 另外在變量的聲明中添加了volatile關(guān)鍵字。關(guān)于volatile簡(jiǎn)單說兩句,由于singleton使用了volatile修飾,一旦一個(gè)線程對(duì)改變做了修改,會(huì)馬上由工作內(nèi)存寫回到主內(nèi)存中,被其他線程所讀取,工作內(nèi)存可以理解為被線程獨(dú)享,主內(nèi)存可以理解為線程共享,被volatile修飾的成員變量可以確保多個(gè)線程都能夠正確的處理,但是該代碼只能在jdk1.5及以上的版本中才能正確執(zhí)行?,F(xiàn)在9都快要發(fā)布了, 相信現(xiàn)在還運(yùn)行在1.5以下的程序應(yīng)該不是很多了,應(yīng)該影響不大, 這里只是提一下。 volatile關(guān)鍵字會(huì)禁止指令重排序優(yōu)化,屏蔽到j(luò)ava虛擬機(jī)所做的一下代碼優(yōu)化,可能會(huì)導(dǎo)致運(yùn)行效率降低, 這看起來也不是一個(gè)非常完美的實(shí)現(xiàn)方式。
餓漢式單例和懶漢式單例的比較
- 餓漢式單例在類加載時(shí)就完成了初始化操作,因此無須考慮多線程的并發(fā)訪問問題,正是由于它一開始就進(jìn)行了實(shí)例化操作,從資源的利用和加載的時(shí)間上考慮可能比懶漢式要差一點(diǎn)
- 懶漢式單例實(shí)現(xiàn)了延遲加載,但是在多線程并發(fā)訪問的情況下對(duì)性能還是有一定的影響。
既然餓漢式和懶漢式都存在自己的問題,有沒有一種方式能解決它們的問題呢,當(dāng)然有,看下面代碼:
通過使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例
public class StaticInnerClass {
private StaticInnerClass(){}
private static class InnerClass{
private static final StaticInnerClass singleton = new StaticInnerClass() ;
}
public static StaticInnerClass getInstance(){
return InnerClass.singleton ;
}
}
調(diào)用getInstance()方法時(shí),才進(jìn)行實(shí)例的初始化操作,由于是static變量只會(huì)進(jìn)行一次初始化操作,有JVM來保證線程的安全性。 即實(shí)現(xiàn)了延遲加載,有可以保證線程的安全。
枚舉實(shí)現(xiàn)單例
//單例類
class Resource{
public Resource(){
System.out.println("resouce 構(gòu)造方法");
}
}
//枚舉類生成Resource的單個(gè)實(shí)例
public enum EnumSingleton{
INSTANCE ;
private Resource resource ;
private EnumSingleton(){
System.out.println("EnumSingleton 構(gòu)造方法執(zhí)行");
resource = new Resource() ;
}
public Resource getInstance(){
return resource ;
}
}
//測(cè)試類及運(yùn)行結(jié)果
public class Test {
public static void main(String[] args) {
Resource s1 = EnumSingleton.INSTANCE.getInstance() ;
Resource s4 = EnumSingleton.INSTANCE.getInstance() ;
Resource s3 = EnumSingleton.INSTANCE.getInstance() ;
Resource s2 = EnumSingleton.INSTANCE.getInstance() ;
if(s1==s2 && s2 ==s3 && s3==s4 && s1 ==s4){
System.out.println("同一個(gè)引用");
}
}
}
//運(yùn)行結(jié)果
EnumSingleton 構(gòu)造方法執(zhí)行
resouce 構(gòu)造方法
同一個(gè)引用
在枚舉類中構(gòu)造方法私有化,外部無法訪問,在訪問枚舉實(shí)例時(shí)會(huì)執(zhí)行構(gòu)造方法,實(shí)例化Resouce對(duì)象,枚舉實(shí)例都是static final類型的,只能被實(shí)例化一次。這也是effective java中推薦的方式。
單例模式的優(yōu)點(diǎn):
- 在內(nèi)存中只有一個(gè)實(shí)例,減少了內(nèi)存的開支,無須頻繁的創(chuàng)建、銷毀
- 減少了系統(tǒng)的性能開銷,避免了資源的多重占用
缺點(diǎn):
- 沒有接口,擴(kuò)展相對(duì)比較困難,違背了單一職責(zé)的原則
使用場(chǎng)景:
要求一個(gè)類有且僅有一個(gè)對(duì)象,只允許有一個(gè)公共的訪問節(jié)點(diǎn)。 可以使用單例模式。
少年聽雨歌樓上,紅燭昏羅帳。
壯年聽雨客舟中,江闊云低,斷雁叫西風(fēng)。
感謝支持!
---起個(gè)名忒難