詳解Java實現(xiàn)單例的五種方式
1. 什么是單例模式
單例模式指的是在應用整個生命周期內只能存在一個實例。單例模式是一種被廣泛使用的設計模式。他有很多好處,能夠避免實例對象的重復創(chuàng)建,減少創(chuàng)建實例的系統(tǒng)開銷,節(jié)省內存。
單例模式的要求有三點:
- 某個類只能有一個實例
- 它必須自行創(chuàng)建這個實例
- 他必須自行向整個系統(tǒng)提供整個實例
2. 單例模式和靜態(tài)類的區(qū)別
首先理解一下什么是靜態(tài)類,靜態(tài)類就是一個類里面都是靜態(tài)方法和靜態(tài)field,構造器被private修飾,因此不能被實例化。Math類就是一個靜態(tài)類。
知道了什么是靜態(tài)類后,來說一下他們兩者之間的區(qū)別:
1)首先單例模式會提供給你一個全局唯一的對象,靜態(tài)類只是提供給你很多靜態(tài)方法,這些方法不用創(chuàng)建對象,通過類就可以直接調用;
2)單例模式的靈活性更高,方法可以被override,因為靜態(tài)類都是靜態(tài)方法,所以不能被override;
3)如果是一個非常重的對象,單例模式可以懶加載,靜態(tài)類就無法做到;
那么時候時候應該用靜態(tài)類,什么時候應該用單例模式呢?首先如果你只是想使用一些工具方法,那么最好用靜態(tài)類,靜態(tài)類比單例類更快,因為靜態(tài)的綁定是在編譯期進行的。如果你要維護狀態(tài)信息,或者訪問資源時,應該選用單例模式。還可以這樣說,當你需要面向對象的能力時(比如繼承、多態(tài))時,選用單例類,當你僅僅是提供一些方法時選用靜態(tài)類。
3.如何實現(xiàn)單例模式
1. 餓漢模式
所謂餓漢模式就是立即加載,一般情況下再調用getInstancef方法之前就已經(jīng)產(chǎn)生了實例,也就是在類加載的時候已經(jīng)產(chǎn)生了。這種模式的缺點很明顯,就是占用資源,當單例類很大的時候,其實我們是想使用的時候再產(chǎn)生實例。因此這種方式適合占用資源少,在初始化的時候就會被用到的類。
class SingletonHungary {
private static SingletonHungary singletonHungary = new SingletonHungary();
//將構造器設置為private禁止通過new進行實例化
private SingletonHungary() {
}
public static SingletonHungary getInstance() {
return singletonHungary;
}
}
2. 懶漢模式
懶漢模式就是延遲加載,也叫懶加載。在程序需要用到的時候再創(chuàng)建實例,這樣保證了內存不會被浪費。針對懶漢模式,這里給出了5種實現(xiàn)方式,有些實現(xiàn)方式是線程不安全的,也就是說在多線程并發(fā)的環(huán)境下可能出現(xiàn)資源同步問題。
首先第一種方式,在單線程下沒問題,在多線程下就出現(xiàn)問題了。
// 單例模式的懶漢實現(xiàn)1--線程不安全
class SingletonLazy1 {
private static SingletonLazy1 singletonLazy;
private SingletonLazy1() {
}
public static SingletonLazy1 getInstance() {
if (null == singletonLazy) {
try {
// 模擬在創(chuàng)建對象之前做一些準備工作
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
singletonLazy = new SingletonLazy1();
}
return singletonLazy;
}
}
我們模擬10個異步線程測試一下:
public class SingletonLazyTest {
public static void main(String[] args) {
Thread2[] ThreadArr = new Thread2[10];
for (int i = 0; i < ThreadArr.length; i++) {
ThreadArr[i] = new Thread2();
ThreadArr[i].start();
}
}
}
// 測試線程
class Thread2 extends Thread {
@Override
public void run() {
System.out.println(SingletonLazy1.getInstance().hashCode());
}
}
運行結果:
124191239
124191239
872096466
1603289047
1698032342
1913667618
371739364
124191239
1723650563
367137303
可以看到他們的hashCode不都是一樣的,說明在多線程環(huán)境下,產(chǎn)生了多個對象,不符合單例模式的要求。
那么如何使線程安全呢?第二種方法,我們使用synchronized關鍵字對getInstance方法進行同步。
// 單例模式的懶漢實現(xiàn)2--線程安全
// 通過設置同步方法,效率太低,整個方法被加鎖
class SingletonLazy2 {
private static SingletonLazy2 singletonLazy;
private SingletonLazy2() {
}
public static synchronized SingletonLazy2 getInstance() {
try {
if (null == singletonLazy) {
// 模擬在創(chuàng)建對象之前做一些準備工作
Thread.sleep(1000);
singletonLazy = new SingletonLazy2();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return singletonLazy;
}
}
使用上面的測試類,測試結果:
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
可以看到,這種方式達到了線程安全。但是缺點就是效率太低,是同步運行的,下個線程想要取得對象,就必須要等上一個線程釋放,才可以繼續(xù)執(zhí)行。
那我們可以不對方法加鎖,而是將里面的代碼加鎖,也可以實現(xiàn)線程安全。但這種方式和同步方法一樣,也是同步運行的,效率也很低。
// 單例模式的懶漢實現(xiàn)3--線程安全
// 通過設置同步代碼塊,效率也太低,整個代碼塊被加鎖
class SingletonLazy3 {
private static SingletonLazy3 singletonLazy;
private SingletonLazy3() {
}
public static SingletonLazy3 getInstance() {
try {
synchronized (SingletonLazy3.class) {
if (null == singletonLazy) {
// 模擬在創(chuàng)建對象之前做一些準備工作
Thread.sleep(1000);
singletonLazy = new SingletonLazy3();
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singletonLazy;
}
}
我們來繼續(xù)優(yōu)化代碼,我們只給創(chuàng)建對象的代碼進行加鎖,但是這樣能保證線程安全么?
// 單例模式的懶漢實現(xiàn)4--線程不安全
// 通過設置同步代碼塊,只同步創(chuàng)建實例的代碼
// 但是還是有線程安全問題
class SingletonLazy4 {
private static SingletonLazy4 singletonLazy;
private SingletonLazy4() {
}
public static SingletonLazy4 getInstance() {
try {
if (null == singletonLazy) { //代碼1
// 模擬在創(chuàng)建對象之前做一些準備工作
Thread.sleep(1000);
synchronized (SingletonLazy4.class) {
singletonLazy = new SingletonLazy4(); //代碼2
}
}
} catch (InterruptedException e) {
// TODO: handle exception
}
return singletonLazy;
}
}
我們來看一下運行結果:
1210004989
1425839054
1723650563
389001266
1356914048
389001266
1560241484
278778395
124191239
367137303
從結果看來,這種方式不能保證線程安全,為什么呢?我們假設有兩個線程A和B同時走到了‘代碼1',因為此時對象還是空的,所以都能進到方法里面,線程A首先搶到鎖,創(chuàng)建了對象。釋放鎖后線程B拿到了鎖也會走到‘代碼2',也創(chuàng)建了一個對象,因此多線程環(huán)境下就不能保證單例了。
讓我們來繼續(xù)優(yōu)化一下,既然上述方式存在問題,那我們在同步代碼塊里面再一次做一下null判斷不就行了,這種方式就是我們的DCL雙重檢查鎖機制。
//單例模式的懶漢實現(xiàn)5--線程安全
//通過設置同步代碼塊,使用DCL雙檢查鎖機制
//使用雙檢查鎖機制成功的解決了單例模式的懶漢實現(xiàn)的線程不安全問題和效率問題
//DCL 也是大多數(shù)多線程結合單例模式使用的解決方案
//第一個if判斷的作用:是為了提高程序的 效率,當SingletonLazy5對象被創(chuàng)建以后,再獲取SingletonLazy5對象時就不用去驗證同步代碼塊的鎖及后面的代碼,直接返回SingletonLazy5對象
//第二個if判斷的作用:是為了解決多線程下的安全性問題,也就是保證對象的唯一。
class SingletonLazy5 {
private static volatile SingletonLazy5 singletonLazy;
private SingletonLazy5() {
}
public static SingletonLazy5 getInstance() {
try {
if (null == singletonLazy) {
// 模擬在創(chuàng)建對象之前做一些準備工作
Thread.sleep(1000);
synchronized (SingletonLazy5.class) {
if(null == singletonLazy) {
singletonLazy = new SingletonLazy5();
}
}
}
} catch (InterruptedException e) {
}
return singletonLazy;
}
}
運行結果:
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
我們可以看到DCL雙重檢查鎖機制很好的解決了懶加載單例模式的效率問題和線程安全問題。這也是我們最常用到的方式。
volatile關鍵字
這里注意到在定義singletonLazy的時候用到了volatile關鍵字,這是為了防止指令重排序的,為什么要這么做呢,我們來看一個場景:
代碼走到了 singletonLazy = new SingletonLazy5();看起來是一句話,但這并不是一個原子操作(要么全部執(zhí)行完,要么全部不執(zhí)行,不能執(zhí)行一半),這句話被編譯成8條匯編指令,大致做了3件事情:
1.給SingletonLazy5的實例分配內存。
2.初始化SingletonLazy5的構造器
3.將singletonLazy對象指向分配的內存空間(注意到這步instance就非null了)。
由于Java編譯器允許處理器亂序執(zhí)行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主內存回寫順序的規(guī)定,上面的第二點和第三點的順序是無法保證的,也就是說,執(zhí)行順序可能是1-2-3也可能是1-3-2,如果是后者,并且在3執(zhí)行完畢、2未執(zhí)行之前,被切換到線程二上,這時候singletonLazy因為已經(jīng)在線程一內執(zhí)行過了第三點,singletonLazy已經(jīng)是非空了,所以線程二直接拿走singletonLazy,然后使用,然后順理成章地報錯,而且這種難以跟蹤難以重現(xiàn)的錯誤估計調試上一星期都未必能找得出來。
DCL的寫法來實現(xiàn)單例是很多技術書、教科書(包括基于JDK1.4以前版本的書籍)上推薦的寫法,實際上是不完全正確的。的確在一些語言(譬如C語言)上DCL是可行的,取決于是否能保證2、3步的順序。在JDK1.5之后,官方已經(jīng)注意到這種問題,因此調整了JMM、具體化了volatile關鍵字,因此如果JDK是1.5或之后的版本,只需要將singletonLazy的定義加上volatile關鍵字,就可以保證每次都去singletonLazy都從主內存讀取,并且可以禁止重排序,就可以使用DCL的寫法來完成單例模式。當然volatile或多或少也會影響到性能,最重要的是我們還要考慮JDK1.42以及之前的版本,所以單例模式寫法的改進還在繼續(xù)。
3. 靜態(tài)內部類
基于上面的考慮,我們可以使用靜態(tài)內部類實現(xiàn)單例模式,代碼如下:
//使用靜態(tài)內部類實現(xiàn)單例模式--線程安全
class SingletonStaticInner {
private SingletonStaticInner() {
}
private static class SingletonInner {
private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
}
public static SingletonStaticInner getInstance() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return SingletonInner.singletonStaticInner;
}
}
可以看到使用這種方式我們沒有顯式的進行任何同步操作,那他是如何保證線程安全呢?和餓漢模式一樣,是靠JVM保證類的靜態(tài)成員只能被加載一次的特點,這樣就從JVM層面保證了只會有一個實例對象。那么問題來了,這種方式和餓漢模式又有什么區(qū)別呢?不也是立即加載么?實則不然,加載一個類時,其內部類不會同時被加載。一個類被加載,當且僅當其某個靜態(tài)成員(靜態(tài)域、構造器、靜態(tài)方法等)被調用時發(fā)生。
可以說這種方式是實現(xiàn)單例模式的最優(yōu)解。
4. 靜態(tài)代碼塊
這里提供了靜態(tài)代碼塊實現(xiàn)單例模式。這種方式和第一種類似,也是一種餓漢模式。
//使用靜態(tài)代碼塊實現(xiàn)單例模式
class SingletonStaticBlock {
private static SingletonStaticBlock singletonStaticBlock;
static {
singletonStaticBlock = new SingletonStaticBlock();
}
public static SingletonStaticBlock getInstance() {
return singletonStaticBlock;
}
}
5. 序列化與反序列化
LZ為什么要提序列化和反序列化呢?因為單例模式雖然能保證線程安全,但在序列化和反序列化的情況下會出現(xiàn)生成多個對象的情況。運行下面的測試類,
public class SingletonStaticInnerSerializeTest {
public static void main(String[] args) {
try {
SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance();
System.out.println(serialize.hashCode());
//序列化
FileOutputStream fo = new FileOutputStream("tem");
ObjectOutputStream oo = new ObjectOutputStream(fo);
oo.writeObject(serialize);
oo.close();
fo.close();
//反序列化
FileInputStream fi = new FileInputStream("tem");
ObjectInputStream oi = new ObjectInputStream(fi);
SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject();
oi.close();
fi.close();
System.out.println(serialize2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//使用匿名內部類實現(xiàn)單例模式,在遇見序列化和反序列化的場景,得到的不是同一個實例
//解決這個問題是在序列化的時候使用readResolve方法,即去掉注釋的部分
class SingletonStaticInnerSerialize implements Serializable {
/**
* 2018年03月28日
*/
private static final long serialVersionUID = 1L;
private static class InnerClass {
private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize();
}
public static SingletonStaticInnerSerialize getInstance() {
return InnerClass.singletonStaticInnerSerialize;
}
// protected Object readResolve() {
// System.out.println("調用了readResolve方法");
// return InnerClass.singletonStaticInnerSerialize;
// }
}
可以看到:
865113938
1078694789
結果表明的確是兩個不同的對象實例,違背了單例模式,那么如何解決這個問題呢?解決辦法就是在反序列化中使用readResolve()方法,將上面的注釋代碼去掉,再次運行:
865113938
調用了readResolve方法
865113938
問題來了,readResolve()方法到底是何方神圣,其實當JVM從內存中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來返回我們指定好的對象了, 單例規(guī)則也就得到了保證。readResolve()的出現(xiàn)允許程序員自行控制通過反序列化得到的對象。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
Java ScheduledExecutorService的具體使用
ScheduledExecutorService有線程池的特性,也可以實現(xiàn)任務循環(huán)執(zhí)行,本文主要介紹了Java ScheduledExecutorService的具體使用,具有一定的參考價值,感興趣的可以了解一下2023-05-05
Spring中@RabbitHandler和@RabbitListener的區(qū)別詳析
@RabbitHandler是用于處理消息的方法注解,它與@RabbitListener注解一起使用,這篇文章主要給大家介紹了關于Spring中@RabbitHandler和@RabbitListener區(qū)別的相關資料,需要的朋友可以參考下2024-02-02
Java創(chuàng)建類模式_動力節(jié)點Java學院整理
這篇文章主要為大家詳細介紹了Java創(chuàng)建類模式的相關方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
java日期格式化SimpleDateFormat的使用詳解
這篇文章主要介紹了java SimpleDateFormat使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-05-05

