復制來自 http://blog.csdn.net/cselmu9/article/details/51366946
在所有的設計模式中,單例模式是我們在項目開發中最為常見的設計模式之一,而單例模式有很多種實現方式,你是否都了解呢?高並發下如何保證單例模式的線程安全性呢?如何保證序列化后的單例對象在反序列化后任然是單例的呢?這些問題在看了本文之后都會一一的告訴你答案,趕快來閱讀吧!
什么是單例模式?
在文章開始之前我們還是有必要介紹一下什么是單例模式。單例模式是為確保一個類只有一個實例,並為整個系統提供一個全局訪問點的一種模式方法。
從概念中體現出了單例的一些特點:
(1)、在任何情況下,單例類永遠只有一個實例存在
(2)、單例需要有能力為整個系統提供這一唯一實例
為了便於讀者更好的理解這些概念,下面給出這么一段內容敘述:
在計算機系統中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每台計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每台計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。
正是由於這個特點,單例對象通常作為程序中的存放配置信息的載體,因為它能保證其他對象讀到一致的信息。例如在某個服務器程序中,該服務器的配置信息可能存放在數據庫或文件中,這些配置數據由某個單例對象統一讀取,服務進程中的其他對象如果要獲取這些配置信息,只需訪問該單例對象即可。這種方式極大地簡化了在復雜環境 下,尤其是多線程環境下的配置管理,但是隨着應用場景的不同,也可能帶來一些同步問題。
各式各樣的單例實現
溫馨提示:本文敘述中涉及到的相關源碼可以在這里進行下載源碼,讀者可免積分下載。
1、餓漢式單例
餓漢式單例是指在方法調用前,實例就已經創建好了。下面是實現代碼:
- package org.mlinge.s01;
- public class MySingleton {
- private static MySingleton instance = new MySingleton();
- private MySingleton(){}
- public static MySingleton getInstance() {
- return instance;
- }
- }
以上是單例的餓漢式實現,我們來看看餓漢式在多線程下的執行情況,給出一段多線程的執行代碼:
- 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();
- }
- }
- }
以上代碼運行結果:
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
從運行結果可以看出實例變量額hashCode值一致,這說明對象是同一個,餓漢式單例實現了。
2、懶漢式單例
懶漢式單例是指在方法調用獲取實例時才創建實例,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。下面是實現代碼:
- 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;
- }
- }
這里實現了懶漢式的單例,但是熟悉多線程並發編程的朋友應該可以看出,在多線程並發下這樣的實現是無法保證實例實例唯一的,甚至可以說這樣的失效是完全錯誤的,下面我們就來看一下多線程並發下的執行情況,這里為了看到效果,我們對上面的代碼做一小點修改:
- package org.mlinge.s02;
- public class MySingleton {
- private static MySingleton instance = null;
- private MySingleton(){}
- public static MySingleton getInstance() {
- try {
- if(instance != null){//懶漢式
- }else{
- //創建實例之前可能會有一些准備性的耗時工作
- Thread.sleep(300);
- instance = new MySingleton();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return instance;
- }
- }
這里假設在創建實例前有一些准備性的耗時工作要處理,多線程調用:
- 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();
- }
- }
- }
執行結果如下:
- 1210420568
- 1210420568
- 1935123450
- 1718900954
- 1481297610
- 1863264879
- 369539795
- 1210420568
- 1210420568
- 602269801
從這里執行結果可以看出,單例的線程安全性並沒有得到保證,那要怎么解決呢?
3、線程安全的懶漢式單例
要保證線程安全,我們就得需要使用同步鎖機制,下面就來看看我們如何一步步的解決 存在線程安全問題的懶漢式單例(錯誤的單例)。
(1)、 方法中聲明synchronized關鍵字
出現非線程安全問題,是由於多個線程可以同時進入getInstance()方法,那么只需要對該方法進行synchronized的鎖同步即可:
- package org.mlinge.s03;
- public class MySingleton {
- private static MySingleton instance = null;
- private MySingleton(){}
- public synchronized static MySingleton getInstance() {
- try {
- if(instance != null){//懶漢式
- }else{
- //創建實例之前可能會有一些准備性的耗時工作
- Thread.sleep(300);
- instance = new MySingleton();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return instance;
- }
- }
此時任然使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s03包下運行,執行結果如下:
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
- 1689058373
從執行結果上來看,問題已經解決了,但是這種實現方式的運行效率會很低。同步方法效率低,那我們考慮使用同步代碼塊來實現:
(2)、 同步代碼塊實現
- 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{
- //創建實例之前可能會有一些准備性的耗時工作
- Thread.sleep(300);
- instance = new MySingleton();
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return instance;
- }
- }
這里的實現能夠保證多線程並發下的線程安全性,但是這樣的實現將全部的代碼都被鎖上了,同樣的效率很低下。
(3)、 針對某些重要的代碼來進行單獨的同步(可能非線程安全)
針對某些重要的代碼進行單獨的同步,而不是全部進行同步,可以極大的提高執行效率,我們來看一下:
- package org.mlinge.s04;
- public class MySingleton {
- private static MySingleton instance = null;
- private MySingleton(){}
- public static MySingleton getInstance() {
- try {
- if(instance != null){//懶漢式
- }else{
- //創建實例之前可能會有一些准備性的耗時工作
- Thread.sleep(300);
- synchronized (MySingleton.class) {
- instance = new MySingleton();
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return instance;
- }
- }
此時同樣使用前面驗證多線程下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s04包下運行,執行結果如下:
- 1481297610
- 397630378
- 1863264879
- 1210420568
- 1935123450
- 369539795
- 590202901
- 1718900954
- 1689058373
- 602269801
從運行結果來看,這樣的方法進行代碼塊同步,代碼的運行效率是能夠得到提升,但是卻沒能保住線程的安全性。看來還得進一步考慮如何解決此問題。
(4)、 Double Check Locking 雙檢查鎖機制(推薦)
為了達到線程安全,又能提高代碼執行效率,我們這里可以采用DCL的雙檢查鎖機制來完成,代碼實現如下:
- package org.mlinge.s05;
- public class MySingleton {
- //使用volatile關鍵字保其可見性
- volatile private static MySingleton instance = null;
- private MySingleton(){}
- public static MySingleton getInstance() {
- try {
- if(instance != null){//懶漢式
- }else{
- //創建實例之前可能會有一些准備性的耗時工作
- Thread.sleep(300);
- synchronized (MySingleton.class) {
- if(instance == null){//二次檢查
- instance = new MySingleton();
- }
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return instance;
- }
- }
將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s05包下運行,執行結果如下:
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
- 369539795
從運行結果來看,該中方法保證了多線程並發下的線程安全性。
這里在聲明變量時使用了volatile關鍵字來保證其線程間的可見性;在同步代碼塊中使用二次檢查,以保證其不被重復實例化。集合其二者,這種實現方式既保證了其高效性,也保證了其線程安全性。
4、使用靜態內置類實現單例模式
DCL解決了多線程並發下的線程安全問題,其實使用其他方式也可以達到同樣的效果,代碼實現如下:
- package org.mlinge.s06;
- public class MySingleton {
- //內部類
- private static class MySingletonHandler{
- private static MySingleton instance = new MySingleton();
- }
- private MySingleton(){}
- public static MySingleton getInstance() {
- return MySingletonHandler.instance;
- }
- }
以上代碼就是使用靜態內置類實現了單例模式,這里將前面驗證多線程下執行情況的MyThread類放入到org.mlinge.s06包下運行,執行結果如下:
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
從運行結果來看,靜態內部類實現的單例在多線程並發下單個實例得到了保證。
5、序列化與反序列化的單例模式實現
靜態內部類雖然保證了單例在多線程並發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的。
代碼實現如下:
- package org.mlinge.s07;
- import java.io.Serializable;
- public class MySingleton implements Serializable {
- private static final long serialVersionUID = 1L;
- //內部類
- private static class MySingletonHandler{
- private static MySingleton instance = new MySingleton();
- }
- private MySingleton(){}
- public static MySingleton getInstance() {
- return MySingletonHandler.instance;
- }
- }
序列化與反序列化測試代碼:
- 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();
- }
- }
- }
運行以上代碼,得到的結果如下:
- 865113938
- 1442407170
從結果中我們發現,序列號對象的hashCode和反序列化后得到的對象的hashCode值不一樣,說明反序列化后返回的對象是重新實例化的,單例被破壞了。那怎么來解決這一問題呢?
解決辦法就是在反序列化的過程中使用readResolve()方法,單例實現的代碼如下:
- package org.mlinge.s07;
- import java.io.ObjectStreamException;
- import java.io.Serializable;
- public class MySingleton implements Serializable {
- private static final long serialVersionUID = 1L;
- //內部類
- private static class MySingletonHandler{
- private static MySingleton instance = new MySingleton();
- }
- private MySingleton(){}
- public static MySingleton getInstance() {
- return MySingletonHandler.instance;
- }
- //該方法在反序列化時會被調用,該方法不是接口定義的方法,有點兒約定俗成的感覺
- protected Object readResolve() throws ObjectStreamException {
- System.out.println("調用了readResolve方法!");
- return MySingletonHandler.instance;
- }
- }
再次運行上面的測試代碼,得到的結果如下:
- 865113938
- 調用了readResolve方法!
- 865113938
從運行結果可知,添加readResolve方法后反序列化后得到的實例和序列化前的是同一個實例,單個實例得到了保證。
6、使用static代碼塊實現單例
靜態代碼塊中的代碼在使用類的時候就已經執行了,所以可以應用靜態代碼塊的這個特性的實現單例設計模式。
- package org.mlinge.s08;
- public class MySingleton{
- private static MySingleton instance = null;
- private MySingleton(){}
- static{
- instance = new MySingleton();
- }
- public static MySingleton getInstance() {
- return instance;
- }
- }
測試代碼如下:
- 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();
- }
- }
- }
運行結果如下:
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
- 1718900954
從運行結果看,單例的線程安全性得到了保證。
7、使用枚舉數據類型實現單例模式
枚舉enum和靜態代碼塊的特性相似,在使用枚舉時,構造方法會被自動調用,利用這一特性也可以實現單例:
- package org.mlinge.s09;
- public enum EnumFactory{
- singletonFactory;
- private MySingleton instance;
- private EnumFactory(){//枚舉類的構造方法在類加載是被實例化
- instance = new MySingleton();
- }
- public MySingleton getInstance(){
- return instance;
- }
- }
- class MySingleton{//需要獲實現單例的類,比如數據庫連接Connection
- public MySingleton(){}
- }
測試代碼如下:
- 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();
- }
- }
- }
執行后得到的結果:
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
- 1481297610
運行結果表明單例得到了保證,但是這樣寫枚舉類被完全暴露了,據說違反了“職責單一原則”,那我們來看看怎么進行改造呢。
8、完善使用enum枚舉實現單例模式
不暴露枚舉類實現細節的封裝代碼如下:
- package org.mlinge.s10;
- public class ClassFactory{
- private enum MyEnumSingleton{
- singletonFactory;
- private MySingleton instance;
- private MyEnumSingleton(){//枚舉類的構造方法在類加載是被實例化
- instance = new MySingleton();
- }
- public MySingleton getInstance(){
- return instance;
- }
- }
- public static MySingleton getInstance(){
- return MyEnumSingleton.singletonFactory.getInstance();
- }
- }
- class MySingleton{//需要獲實現單例的類,比如數據庫連接Connection
- public MySingleton(){}
- }
驗證單例實現的代碼如下:
- 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();
- }
- }
- }
驗證結果:
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
- 1935123450
驗證結果表明,完善后的單例實現更為合理。
以上就是本文要介紹的所有單例模式的實現,相信認真閱讀的讀者都已經明白文章開頭所引入的那幾個問題了,祝大家讀得開心:-D!
備注:本文的編寫思路和實例源碼參照《Java多線程編程核心技術》-(高洪岩)一書中第六章的學習案例撰寫。