在所有的設(shè)計模式中,單例模式是我們在項目開發(fā)中最為常見的設(shè)計模式之一,而單例模式有很多種實現(xiàn)方式,你是否都了解呢?高并發(fā)下如何保證單例模式的線程安全性呢?如何保證序列化后的單例對象在反序列化后任然是單例的呢?這些問題在看了本文之后都會一一的告訴你答案,趕快來閱讀吧!
什么是單例模式?
在文章開始之前我們還是有必要介紹一下什么是單例模式。單例模式是為確保一個類只有一個實例,并為整個系統(tǒng)提供一個全局訪問點的一種模式方法。
從概念中體現(xiàn)出了單例的一些特點:
(1)、在任何情況下,單例類永遠(yuǎn)只有一個實例存在
(2)、單例需要有能力為整個系統(tǒng)提供這一唯一實例
為了便于讀者更好的理解這些概念,下面給出這么一段內(nèi)容敘述:
在計算機(jī)系統(tǒng)中,線程池、緩存、日志對象、對話框、打印機(jī)、顯卡的驅(qū)動程序?qū)ο蟪1辉O(shè)計成單例。這些應(yīng)用都或多或少具有資源管理器的功能。每臺計算機(jī)可以有若干個打印機(jī),但只能有一個PRinter Spooler,以避免兩個打印作業(yè)同時輸出到打印機(jī)中。每臺計算機(jī)可以有若干通信端口,系統(tǒng)應(yīng)當(dāng)集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調(diào)用。總之,選擇單例模式就是為了避免不一致狀態(tài),避免政出多頭。正是由于這個特點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。例如在某個服務(wù)器程序中,該服務(wù)器的配置信息可能存放在數(shù)據(jù)庫或文件中,這些配置數(shù)據(jù)由某個單例對象統(tǒng)一讀取,服務(wù)進(jìn)程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。這種方式極大地簡化了在復(fù)雜環(huán)境 下,尤其是多線程環(huán)境下的配置管理,但是隨著應(yīng)用場景的不同,也可能帶來一些同步問題。
各式各樣的單例實現(xiàn)
溫馨提示:本文敘述中涉及到的相關(guān)源碼可以在這里進(jìn)行下載源碼,讀者可免積分下載。
1、餓漢式單例
餓漢式單例是指在方法調(diào)用前,實例就已經(jīng)創(chuàng)建好了。下面是實現(xiàn)代碼:
[java] view plain copy print?
package org.mlinge.s01; public class MySingleton { private static MySingleton instance = new MySingleton(); private MySingleton(){} public static MySingleton getInstance() { return instance; } } 以上是單例的餓漢式實現(xiàn),我們來看看餓漢式在多線程下的執(zhí)行情況,給出一段多線程的執(zhí)行代碼:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s01; public class MyThread extends Thread{ @Override public void run() { System.out.println(MySingleton.getInstance().hashCode()); } public static void main(String[] args) { MyThread[] mts = new MyThread[10]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new MyThread(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } } 以上代碼運行結(jié)果:
[plain] view%20plain copy print?![在CODE上查看代碼片]()
1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 從運行結(jié)果可以看出實例變量額hashCode值一致,這說明對象是同一個,餓漢式單例實現(xiàn)了。2、懶漢式單例
懶漢式單例是指在方法調(diào)用獲取實例時才創(chuàng)建實例,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。下面是實現(xiàn)代碼:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s02; public class MySingleton { private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { if(instance == null){//懶漢式 instance = new MySingleton(); } return instance; } } 這里實現(xiàn)了懶漢式的單例,但是熟悉多線程并發(fā)編程的朋友應(yīng)該可以看出,在多線程并發(fā)下這樣的實現(xiàn)是無法保證實例實例唯一的,甚至可以說這樣的失效是完全錯誤的,下面我們就來看一下多線程并發(fā)下的執(zhí)行情況,這里為了看到效果,我們對上面的代碼做一小點修改:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s02; public class MySingleton { private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //創(chuàng)建實例之前可能會有一些準(zhǔn)備性的耗時工作 Thread.sleep(300); instance = new MySingleton(); } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } } 這里假設(shè)在創(chuàng)建實例前有一些準(zhǔn)備性的耗時工作要處理,多線程調(diào)用:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s02; public class MyThread extends Thread{ @Override public void run() { System.out.println(MySingleton.getInstance().hashCode()); } public static void main(String[] args) { MyThread[] mts = new MyThread[10]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new MyThread(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } } 執(zhí)行結(jié)果如下:
[plain] view%20plain copy print?![在CODE上查看代碼片]()
1210420568 1210420568 1935123450 1718900954 1481297610 1863264879 369539795 1210420568 1210420568 602269801 從這里執(zhí)行結(jié)果可以看出,單例的線程安全性并沒有得到保證,那要怎么解決呢?3、線程安全的懶漢式單例
要保證線程安全,我們就得需要使用同步鎖機(jī)制,下面就來看看我們?nèi)绾我徊讲降慕鉀Q%20存在線程安全問題的懶漢式單例(錯誤的單例)。
(1)、%20方法中聲明synchronized關(guān)鍵字
出現(xiàn)非線程安全問題,是由于多個線程可以同時進(jìn)入getInstance()方法,那么只需要對該方法進(jìn)行synchronized的鎖同步即可:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s03; public class MySingleton { private static MySingleton instance = null; private MySingleton(){} public synchronized static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //創(chuàng)建實例之前可能會有一些準(zhǔn)備性的耗時工作 Thread.sleep(300); instance = new MySingleton(); } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } } 此時任然使用前面驗證多線程下執(zhí)行情況的MyThread類來進(jìn)行驗證,將其放入到org.mlinge.s03包下運行,執(zhí)行結(jié)果如下:
[plain] view%20plain copy print?![在CODE上查看代碼片]()
1689058373 1689058373 1689058373 1689058373 1689058373 1689058373 1689058373 1689058373 1689058373 1689058373 從執(zhí)行結(jié)果上來看,問題已經(jīng)解決了,但是這種實現(xiàn)方式的運行效率會很低。同步方法效率低,那我們考慮使用同步代碼塊來實現(xiàn):
(2)、%20同步代碼塊實現(xiàn)
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s03; public class MySingleton { private static MySingleton instance = null; private MySingleton(){} //public synchronized static MySingleton getInstance() { public static MySingleton getInstance() { try { synchronized (MySingleton.class) { if(instance != null){//懶漢式 }else{ //創(chuàng)建實例之前可能會有一些準(zhǔn)備性的耗時工作 Thread.sleep(300); instance = new MySingleton(); } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } } 這里的實現(xiàn)能夠保證多線程并發(fā)下的線程安全性,但是這樣的實現(xiàn)將全部的代碼都被鎖上了,同樣的效率很低下。(3)、%20針對某些重要的代碼來進(jìn)行單獨的同步(可能非線程安全)
針對某些重要的代碼進(jìn)行單獨的同步,而不是全部進(jìn)行同步,可以極大的提高執(zhí)行效率,我們來看一下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s04; public class MySingleton { private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //創(chuàng)建實例之前可能會有一些準(zhǔn)備性的耗時工作 Thread.sleep(300); synchronized (MySingleton.class) { instance = new MySingleton(); } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } } 此時同樣使用前面驗證多線程下執(zhí)行情況的MyThread類來進(jìn)行驗證,將其放入到org.mlinge.s04包下運行,執(zhí)行結(jié)果如下:[plain] view%20plain copy print?![在CODE上查看代碼片]()
1481297610 397630378 1863264879 1210420568 1935123450 369539795 590202901 1718900954 1689058373 602269801 從運行結(jié)果來看,這樣的方法進(jìn)行代碼塊同步,代碼的運行效率是能夠得到提升,但是卻沒能保住線程的安全性。看來還得進(jìn)一步考慮如何解決此問題。(4)、 Double%20Check%20Locking 雙檢查鎖機(jī)制(推薦)
為了達(dá)到線程安全,又能提高代碼執(zhí)行效率,我們這里可以采用DCL的雙檢查鎖機(jī)制來完成,代碼實現(xiàn)如下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s05; public class MySingleton { //使用volatile關(guān)鍵字保其可見性 volatile private static MySingleton instance = null; private MySingleton(){} public static MySingleton getInstance() { try { if(instance != null){//懶漢式 }else{ //創(chuàng)建實例之前可能會有一些準(zhǔn)備性的耗時工作 Thread.sleep(300); synchronized (MySingleton.class) { if(instance == null){//二次檢查 instance = new MySingleton(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return instance; } } 將前面驗證多線程下執(zhí)行情況的MyThread類放入到org.mlinge.s05包下運行,執(zhí)行結(jié)果如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
369539795 369539795 369539795 369539795 369539795 369539795 369539795 369539795 369539795 369539795 從運行結(jié)果來看,該中方法保證了多線程并發(fā)下的線程安全性。這里在聲明變量時使用了volatile關(guān)鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重復(fù)實例化。集合其二者,這種實現(xiàn)方式既保證了其高效性,也保證了其線程安全性。
4、使用靜態(tài)內(nèi)置類實現(xiàn)單例模式
DCL解決了多線程并發(fā)下的線程安全問題,其實使用其他方式也可以達(dá)到同樣的效果,代碼實現(xiàn)如下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s06; public class MySingleton { //內(nèi)部類 private static class MySingletonHandler{ private static MySingleton instance = new MySingleton(); } private MySingleton(){} public static MySingleton getInstance() { return MySingletonHandler.instance; } } 以上代碼就是使用靜態(tài)內(nèi)置類實現(xiàn)了單例模式,這里將前面驗證多線程下執(zhí)行情況的MyThread類放入到org.mlinge.s06包下運行,執(zhí)行結(jié)果如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 從運行結(jié)果來看,靜態(tài)內(nèi)部類實現(xiàn)的單例在多線程并發(fā)下單個實例得到了保證。5、序列化與反序列化的單例模式實現(xiàn)
靜態(tài)內(nèi)部類雖然保證了單例在多線程并發(fā)下的線程安全性,但是在遇到序列化對象時,默認(rèn)的方式運行得到的結(jié)果就是多例的。
代碼實現(xiàn)如下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s07; import java.io.Serializable; public class MySingleton implements Serializable { private static final long serialVersionUID = 1L; //內(nèi)部類 private static class MySingletonHandler{ private static MySingleton instance = new MySingleton(); } private MySingleton(){} public static MySingleton getInstance() { return MySingletonHandler.instance; } } 序列化與反序列化測試代碼:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s07; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class SaveAndReadForSingleton { public static void main(String[] args) { MySingleton singleton = MySingleton.getInstance(); File file = new File("MySingleton.txt"); try { FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(singleton); fos.close(); oos.close(); System.out.println(singleton.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } try { FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis); MySingleton rSingleton = (MySingleton) ois.readObject(); fis.close(); ois.close(); System.out.println(rSingleton.hashCode()); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } 運行以上代碼,得到的結(jié)果如下:[sql] view%20plain copy print?![在CODE上查看代碼片]()
865113938 1442407170 從結(jié)果中我們發(fā)現(xiàn),序列號對象的hashCode和反序列化后得到的對象的hashCode值不一樣,說明反序列化后返回的對象是重新實例化的,單例被破壞了。那怎么來解決這一問題呢?解決辦法就是在反序列化的過程中使用readResolve()方法,單例實現(xiàn)的代碼如下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s07; import java.io.ObjectStreamException; import java.io.Serializable; public class MySingleton implements Serializable { private static final long serialVersionUID = 1L; //內(nèi)部類 private static class MySingletonHandler{ private static MySingleton instance = new MySingleton(); } private MySingleton(){} public static MySingleton getInstance() { return MySingletonHandler.instance; } //該方法在反序列化時會被調(diào)用,該方法不是接口定義的方法,有點兒約定俗成的感覺 protected Object readResolve() throws ObjectStreamException { System.out.println("調(diào)用了readResolve方法!"); return MySingletonHandler.instance; } } 再次運行上面的測試代碼,得到的結(jié)果如下:[plain] view%20plain copy print?![在CODE上查看代碼片]()
865113938 調(diào)用了readResolve方法! 865113938 從運行結(jié)果可知,添加readResolve方法后反序列化后得到的實例和序列化前的是同一個實例,單個實例得到了保證。6、使用static代碼塊實現(xiàn)單例
靜態(tài)代碼塊中的代碼在使用類的時候就已經(jīng)執(zhí)行了,所以可以應(yīng)用靜態(tài)代碼塊的這個特性的實現(xiàn)單例設(shè)計模式。
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s08; public class MySingleton{ private static MySingleton instance = null; private MySingleton(){} static{ instance = new MySingleton(); } public static MySingleton getInstance() { return instance; } } 測試代碼如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s08; public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(MySingleton.getInstance().hashCode()); } } public static void main(String[] args) { MyThread[] mts = new MyThread[3]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new MyThread(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } } 運行結(jié)果如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 1718900954 從運行結(jié)果看,單例的線程安全性得到了保證。7、使用枚舉數(shù)據(jù)類型實現(xiàn)單例模式
枚舉enum和靜態(tài)代碼塊的特性相似,在使用枚舉時,構(gòu)造方法會被自動調(diào)用,利用這一特性也可以實現(xiàn)單例:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s09; public enum EnumFactory{ singletonFactory; private MySingleton instance; private EnumFactory(){//枚舉類的構(gòu)造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } class MySingleton{//需要獲實現(xiàn)單例的類,比如數(shù)據(jù)庫連接Connection public MySingleton(){} } 測試代碼如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s09; public class MyThread extends Thread{ @Override public void run() { System.out.println(EnumFactory.singletonFactory.getInstance().hashCode()); } public static void main(String[] args) { MyThread[] mts = new MyThread[10]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new MyThread(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } } 執(zhí)行后得到的結(jié)果:[java] view%20plain copy print?![在CODE上查看代碼片]()
1481297610 1481297610 1481297610 1481297610 1481297610 1481297610 1481297610 1481297610 1481297610 1481297610 運行結(jié)果表明單例得到了保證,但是這樣寫枚舉類被完全暴露了,據(jù)說違反了“職責(zé)單一原則”,那我們來看看怎么進(jìn)行改造呢。8、完善使用enum枚舉實現(xiàn)單例模式
不暴露枚舉類實現(xiàn)細(xì)節(jié)的封裝代碼如下:
[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s10; public class ClassFactory{ private enum MyEnumSingleton{ singletonFactory; private MySingleton instance; private MyEnumSingleton(){//枚舉類的構(gòu)造方法在類加載是被實例化 instance = new MySingleton(); } public MySingleton getInstance(){ return instance; } } public static MySingleton getInstance(){ return MyEnumSingleton.singletonFactory.getInstance(); } } class MySingleton{//需要獲實現(xiàn)單例的類,比如數(shù)據(jù)庫連接Connection public MySingleton(){} } 驗證單例實現(xiàn)的代碼如下:[java] view%20plain copy print?![在CODE上查看代碼片]()
package org.mlinge.s10; public class MyThread extends Thread{ @Override public void run() { System.out.println(ClassFactory.getInstance().hashCode()); } public static void main(String[] args) { MyThread[] mts = new MyThread[10]; for(int i = 0 ; i < mts.length ; i++){ mts[i] = new MyThread(); } for (int j = 0; j < mts.length; j++) { mts[j].start(); } } } 驗證結(jié)果:[java] view%20plain copy print?![在CODE上查看代碼片]()
1935123450 1935123450 1935123450 1935123450 1935123450 1935123450 1935123450 1935123450 1935123450 1935123450 驗證結(jié)果表明,完善后的單例實現(xiàn)更為合理。以上就是本文要介紹的所有單例模式的實現(xiàn),相信認(rèn)真閱讀的讀者都已經(jīng)明白文章開頭所引入的那幾個問題了,祝大家讀得開心:-D!
原文地址:http://blog.csdn.net/cselmu9/article/details/51366946#t11