最近學(xué)習(xí)了Java的幾種常規(guī)的設(shè)計(jì)模式,內(nèi)容較多,思維方式多種多樣,故將所學(xué)整理一下,寫成博客,分享并加深自己的理解與記憶。
首先我們看一下什么是設(shè)計(jì)模式?
設(shè)計(jì)模式(Design pattern)
設(shè)計(jì)模式(Design pattern)是一套被反復(fù)使用、多數(shù)人知曉的、經(jīng)過分類編目的、代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。使用設(shè)計(jì)模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。 毫無疑問,設(shè)計(jì)模式于己于他人于系統(tǒng)都是多贏的;設(shè)計(jì)模式使代碼編制真正工程化;設(shè)計(jì)模式是軟件工程的基石脈絡(luò),如同大廈的結(jié)構(gòu)一樣。
設(shè)計(jì)模式分為三大類:
創(chuàng)建型模式
共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
結(jié)構(gòu)型模式
共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
行為型模式
共十一種:策略模式、模板方法模式、觀察者模式、迭代子模式、責(zé)任鏈模式、命令模式、備忘錄模式、狀態(tài)模式、訪問者模式、中介者模式、解釋器模式。
其實(shí)還有兩類:并發(fā)型模式和線程池模式。
更多關(guān)于設(shè)計(jì)模式的設(shè)計(jì)原則與內(nèi)容,會(huì)在陸續(xù)出完設(shè)計(jì)模式系列最后匯總一下。
接下來,進(jìn)入本篇的主題——單例模式(Singleton)
單例模式(Singleton)
在我們?nèi)粘5墓ぷ髦薪?jīng)常需要在應(yīng)用程序中保持一個(gè)唯一的實(shí)例,如:IO處理,數(shù)據(jù)庫操作,配置文件,工具類,線程池,緩存,日志對象等,由于這些對象都要占用重要的系統(tǒng)資源,所以我們必須限制這些實(shí)例的創(chuàng)建或始終使用一個(gè)公用的實(shí)例,如果創(chuàng)造出來多個(gè)實(shí)例,就會(huì)導(dǎo)致許多問題,比如占用過多資源,不一致的結(jié)果等。這就是我們今天要介紹的——單例模式(Singleton)。

java中單例模式是一種常見的設(shè)計(jì)模式,單例模式分兩種:懶漢式單例、餓漢式單例。
單例模式有以下特點(diǎn):
- 單例類只能有一個(gè)實(shí)例。
- 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
- 單例類必須給所有其他對象提供這一實(shí)例。
餓漢式單例(Eager initialization)
public class Singleton {
//1.將構(gòu)造方法私有化,不允許外部直接創(chuàng)建對象
private Singleton(){
}
//2.創(chuàng)建類的唯一實(shí)例,使用private static修飾
private static Singleton instance=new Singleton();
//3.提供一個(gè)用于獲取實(shí)例的方法,使用public static修飾
public static Singleton getInstance(){
return instance;
}
}
這種方式基于classloder機(jī)制避免了多線程的同步問題,不過,instance在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance顯然沒有達(dá)到lazy loading的效果。
下面提供餓漢式的變種:
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起來差別挺大,其實(shí)和之前差不多,都是在類初始化即實(shí)例化instance。
總結(jié)一下餓漢式的優(yōu)點(diǎn):
- The static initializer is run when the class is initialized, after class loading but before the class is used by any thread.
- There is no need to synchronize the
getInstance()method, meaning all threads will see the same instance and no (expensive) locking is required.
- The
finalkeyword means that the instance cannot be redefined, ensuring that one (and only one) instance ever exists.
懶漢式單例(Lazy initialization)
/*
* 懶漢模式
* 區(qū)別:餓漢模式的特點(diǎn)是加載類時(shí)比較慢,但運(yùn)行時(shí)獲取對象的速度比較快,線程安全
* 懶漢模式的特點(diǎn)是加載類時(shí)比較快,但運(yùn)行時(shí)獲取對象的速度比較慢,線程不安全
*/
public class Singleton {
//1.將構(gòu)造方式私有化,不允許外邊直接創(chuàng)建對象
private Singleton(){
}
//2.聲明類的唯一實(shí)例,使用private static修飾
private static Singleton instance;
//3.提供一個(gè)用于獲取實(shí)例的方法,使用public static修飾
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
這種寫法lazy loading很明顯,但是致命的是在多線程不能正常工作。
其實(shí)懶漢式也是可以寫成線程安全的,代碼如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton() { }
public static SingletongetInstance() {
if (instance == null ) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的lazy loading,但是,遺憾的是,效率很低,99%情況下不需要同步。
寫博客的時(shí)候,發(fā)現(xiàn)了WIKI提供了一個(gè)更加牛的懶漢式升級(jí)版——雙重校驗(yàn)鎖。
public class Singleton {
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;
}
}
雙重校驗(yàn)鎖(Double-checked locking),俗稱雙重檢查鎖定。
其他單例的實(shí)現(xiàn)
登記式單例
//類似Spring里面的方法,將類名注冊,下次從里面直接獲取。
public class Singleton3 {
private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
static{
Singleton3 single = new Singleton3();
map.put(single.getClass().getName(), single);
}
//保護(hù)的默認(rèn)構(gòu)造子
protected Singleton3(){}
//靜態(tài)工廠方法,返還此類惟一的實(shí)例
public static Singleton3 getInstance(String name) {
if(name == null) {
name = Singleton3.class.getName();
System.out.println("name == null"+"--->name="+name);
}
if(map.get(name) == null) {
try {
map.put(name, (Singleton3) Class.forName(name).newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return map.get(name);
}
//一個(gè)示意性的商業(yè)方法
public String about() {
return "Hello, I am RegSingleton.";
}
public static void main(String[] args) {
Singleton3 single3 = Singleton3.getInstance(null);
System.out.println(single3.about());
}
}
登記式單例實(shí)際上維護(hù)了一組單例類的實(shí)例,將這些實(shí)例存放在一個(gè)Map(登記?。┲校瑢τ谝呀?jīng)登記過的實(shí)例,則從Map直接返回,對于沒有登記的,則先登記,然后返回。
枚舉(The enum way)
public enum Singleton {
INSTANCE;
public void execute (String arg) {
// Perform operation here
}
}
居然用枚舉!!看上去好牛逼,通過Singleton.INSTANCE來訪問,這比調(diào)用getInstance()方法簡單多了。這種方式是《Effective Java》作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創(chuàng)建新的對象,可謂是很堅(jiān)強(qiáng)的壁壘啊。
默認(rèn)枚舉實(shí)例的創(chuàng)建是線程安全的,所以不需要擔(dān)心線程安全的問題。但是在枚舉中的其他任何方法的線程安全由程序員自己負(fù)責(zé)。還有防止上面的通過反射機(jī)制調(diào)用私用構(gòu)造器。
這個(gè)版本基本上消除了絕大多數(shù)的問題。代碼也非常簡單,實(shí)在無法不用。
靜態(tài)內(nèi)部類(Static block initialization)
public class Singleton {
private static final Singleton instance;
static {
try {
instance = new Singleton();
} catch (Exception e) {
throw new RuntimeException("Darn, an error occurred!", e);
}
}
public static Singleton getInstance() {
return instance;
}
private Singleton() {
// ...
}
}
這種方式同樣利用了classloder的機(jī)制來保證初始化instance時(shí)只有一個(gè)線程,它跟餓漢式不同的是(很細(xì)微的差別):餓漢式是只要Singleton類被裝載了,那么instance就會(huì)被實(shí)例化(沒有達(dá)到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因?yàn)镾ingletonHolder類沒有被主動(dòng)使用,只有顯示通過調(diào)用getInstance方法時(shí),才會(huì)顯示裝載SingletonHolder類,從而實(shí)例化instance。想象一下,如果實(shí)例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時(shí)就實(shí)例化,因?yàn)槲也荒艽_保Singleton類還可能在其他的地方被主動(dòng)使用從而被加載,那么這個(gè)時(shí)候?qū)嵗痠nstance顯然是不合適的。這個(gè)時(shí)候,這種方式相比餓漢式就顯得很合理。
餓漢式和懶漢式的區(qū)別
這兩種乍看上去非常相似,其實(shí)是有區(qū)別的,主要兩點(diǎn)
1、線程安全:
餓漢式是線程安全的,可以直接用于多線程而不會(huì)出現(xiàn)問題,懶漢式就不行,它是線程不安全的,如果用于多線程可能會(huì)被實(shí)例化多次,失去單例的作用。
如果要把懶漢式用于多線程,有兩種方式保證安全性,一種是在getInstance方法上加同步,另一種是在使用該單例方法前后加雙鎖。
2、資源加載:
餓漢式在類創(chuàng)建的同時(shí)就實(shí)例化一個(gè)靜態(tài)對象出來,不管之后會(huì)不會(huì)使用這個(gè)單例,會(huì)占據(jù)一定的內(nèi)存,相應(yīng)的在調(diào)用時(shí)速度也會(huì)更快,
而懶漢式顧名思義,會(huì)延遲加載,在第一次使用該單例的時(shí)候才會(huì)實(shí)例化對象出來,第一次掉用時(shí)要初始化,如果要做的工作比較多,性能上會(huì)有些延遲,之后就和餓漢式一樣了。
什么是線程安全?
如果你的代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
或者說:一個(gè)類或者程序所提供的接口對于線程來說是原子操作,或者多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性,也就是說我們不用考慮同步的問題,那就是線程安全的。
應(yīng)用
以下是一個(gè)單例類使用的例子,以懶漢式為例
public class TestSingleton {
String name = null;
private TestSingleton() {
}
private static TestSingleton ts = null;
public static TestSingleton getInstance() {
if (ts == null) {
ts = new TestSingleton();
}
return ts;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void printInfo() {
System.out.println("the name is " + name);
}
}
public class TMain {
public static void main(String[] args){
TestStream ts1 = TestSingleton.getInstance();
ts1.setName("jason");
TestStream ts2 = TestSingleton.getInstance();
ts2.setName("0539");
ts1.printInfo();
ts2.printInfo();
if(ts1 == ts2){
System.out.println("創(chuàng)建的是同一個(gè)實(shí)例");
}else{
System.out.println("創(chuàng)建的不是同一個(gè)實(shí)例");
}
}
}
運(yùn)行結(jié)果:

結(jié)論:由結(jié)果可以得知單例模式為一個(gè)面向?qū)ο蟮膽?yīng)用程序提供了對象惟一的訪問點(diǎn),不管它實(shí)現(xiàn)何種功能,整個(gè)應(yīng)用程序都會(huì)同享一個(gè)實(shí)例對象。
參考資料: