Java語言中單例模式的四種寫法

做爲設計模式理論中的Helloworld,相信學習java語言的人,都應該據說過單例模式。單例模式做爲對象的一種建立模式,它的做用是確保某一個類在整個系統中只有一個實例,並且自行實例化並向整個系統提供這個實例。 java

因而可知,單例模式具備如下的特色: apache

  1. 單例類只能有一個實例。
  2. 單例類必須本身建立本身的惟一的實例。
  3. 單例類必須給全部其餘對象提供這一實例。

因爲Java語言的特色,使得單例模式在Java語言的實現上有本身的特色。這些特色主要表如今單例類如何將本身實例化。 設計模式

餓漢式單例類 數組

餓漢式單例類是在Java語言裏實現起來最爲簡便的單例類。其源代碼以下: 安全

public class EagerSingleton {

	/** 經過靜態變量初始化的類實例 */
	private static final EagerSingleton instance = new EagerSingleton();

	/**
	 * 私有的默認構造子
	 */
	private EagerSingleton() {
	}

	/**
	 * 獲取惟一類實例的靜態工廠方法
	 * 
	 * @return
	 */
	public static EagerSingleton getInstance() {
		return instance;
	}

}

由Java語言類的初始化順序可知,在這個類被加載時,靜態變量會被初始化,此時類的私有構造子會被調用。這時候,單例類的惟一實例就被建立出來了。 併發

Java語言中單例類的一個最重要的特色是類的構造子是私有的,從而避免外界使用構造子直接建立出任意多該類的實例。值得指出的是,因爲構造子是私有的,所以該類不能被繼承。 ide

懶漢式單例類 學習

與餓漢式單例類相同之處是,懶漢式單例類的構造子也是私有的。而與餓漢式單例類不一樣的是,懶漢式單例類在第一次被引用時將本身實例化。在懶漢式單例類被加載時,不會將本身實例化。其源代碼以下所示: spa

public class LazySingleton {

	/**
	 * 此時靜態變量不能聲明爲final,由於須要在工廠方法中對它進行實例化
	 */
	private static LazySingleton instance;

	/**
	 * 私有構造子,確保沒法在類外實例化該類
	 */
	private LazySingleton() {
	}

	/**
	 * synchronized關鍵字解決多個線程的同步問題
	 */
	public static synchronized LazySingleton getInstance() {
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}

}

靜態工廠方法中synchronized關鍵字提供的同步是必須的,不然當多個線程同時訪問該方法時,沒法確保得到的老是同一個實例。然而咱們也看到,在全部的代碼路徑中,雖然只有第一次引用的時候須要對instance變量進行實例化,可是synchronized同步機制要求全部的代碼執行路徑都必須先獲取類鎖。在併發訪問比較低時,效果並不顯著,可是當併發訪問量上升時,這裏有可能會成爲併發訪問的瓶頸。 線程

若是本文言盡於此,那實在沒什麼寫的必要,由於以上介紹的知識,你能夠從任何一本介紹單例模式的書查獲得。下面咱們來看看,爲了下降線程同步的開銷,從懶漢式單例模式中引伸出來的兩種頗有意思的單例模式寫法。

延長初始化佔位

這種單例模式的寫法,是著名的《Java Concurrency in Practice》一書中介紹對象的安全發佈時介紹的。咱們先來看它的源代碼。

public class ResourceFactory {

	private static class ResourceHolder {
		public static Resource resource = new Resource();
	}

	public static Resource getResource() {
		return ResourceHolder.resource;
	}

}

要理解上面這種單例類的寫法,你須要先學習一些關於Java虛擬機如何初始化一個類的知識。

在java虛擬機中,類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段。其中,驗證、準備和解析三個部分統稱爲鏈接(Linking)。

加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也被稱爲動態綁定或晚期綁定)。

什麼狀況下須要開始類加載的第一個階段:加載。虛擬機規範中並無進行強制約束,這點能夠交給虛擬機的具體實現來自由把握。可是對於初始化階段,虛擬機規範則是嚴格規定了有且只有四種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):

1)遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這四條字節碼指令最多見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。

2)使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。

3)當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。

4)當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

這四種場景中的行爲稱爲對一個類進行主動引用,除此以外全部引用類的方式,都不會觸發類的初始化,被稱爲被動引用。如下是三個例子:

1)經過子類引用父類的靜態字段,不會致使子類初始化。

2)經過數組定義來引用類,不會觸發此類的初始化。

3)常量在編譯階段會存入調用類的常量池,本質上沒有直接引用到定義常量的類,所以不會觸發定義常量的類的初始化。

(以上摘自《深刻理解Java虛擬機》)

從上面介紹的知識能夠知道,JVM將推遲ResourceHolder類的初始化,直到第一個代碼訪問路徑調用getResource()方法。此時,因爲ResourceHolder.resource是一個讀取靜態字段的主動引用,虛擬機將第一次加載ResourceHolder類,而且經過一個靜態變量來初始化Resource實例。而其餘訪問getResource()方法的代碼路徑,並不須要同步。

不須要額外的同步,可是又能確保對象可見性的正確發佈,這是由Java的虛擬機規範所決定的!上面這種單例模式的寫法,體現出對虛擬機規範的深入理解,實在是專家級別的寫法。

用讀寫鎖編寫的單例模式

在閱讀Struts2源碼的時候,我發現一個有意思的單例類寫法:LoggerFactory。這裏和你們分享一下,先看一下源碼。

package com.opensymphony.xwork2.util.logging;

import com.opensymphony.xwork2.util.logging.jdk.JdkLoggerFactory;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Creates loggers.  Static accessor will lazily try to decide on the best factory if none specified.
 */
public abstract class LoggerFactory {
    
    private static final ReadWriteLock lock = new ReentrantReadWriteLock();
    private static LoggerFactory factory;
    
    public static void setLoggerFactory(LoggerFactory factory) {
        lock.writeLock().lock();
        try {
            LoggerFactory.factory = factory;
        } finally {
            lock.writeLock().unlock();
        }
            
    }
    
    public static Logger getLogger(Class<?> cls) {
        return getLoggerFactory().getLoggerImpl(cls);
    }
    
    public static Logger getLogger(String name) {
        return getLoggerFactory().getLoggerImpl(name);
    }
    
    protected static LoggerFactory getLoggerFactory() {
        lock.readLock().lock();
        try {
            if (factory != null) {
                return factory;
            }
        } finally {
            lock.readLock().unlock();
        }
        lock.writeLock().lock();
        try {
            if (factory == null) {
                try {
                    Class.forName("org.apache.commons.logging.LogFactory");
                    factory = new com.opensymphony.xwork2.util.logging.commons.CommonsLoggerFactory();
                } catch (ClassNotFoundException ex) {
                    // commons logging not found, falling back to jdk logging
                    factory = new JdkLoggerFactory();
                }
            }
            return factory;
        }
        finally {
            lock.writeLock().unlock();
        }
    }
    
    protected abstract Logger getLoggerImpl(Class<?> cls);
    
    protected abstract Logger getLoggerImpl(String name); 

}
它是一個抽象類,來看一個具體的實現類:CommonsLoggerFactory。

package com.opensymphony.xwork2.util.logging.commons;

import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import org.apache.commons.logging.LogFactory;

/**
 * Creates commons-logging-backed loggers
 */
public class CommonsLoggerFactory extends LoggerFactory {

    @Override
    protected Logger getLoggerImpl(Class<?> cls) {
        return new CommonsLogger(LogFactory.getLog(cls));
    }
    
    @Override
    protected Logger getLoggerImpl(String name) {
        return new CommonsLogger(LogFactory.getLog(name));
    }

}

能夠看到,在大多數的代碼路徑下,getLoggerFactory()方法用可重入的讀鎖來進行同步。只在第一次訪問時,使用了可重入的寫鎖來進行同步,進行factory對象的初始化。由於在寫鎖還沒釋放的時候,任何讀鎖的獲取都會被阻塞,這樣就保證了所發佈的factory對象的可見性。

這裏的代碼,很容易就能夠被改形成一個單例模式的寫法。經過非阻塞的讀寫鎖,避免了懶漢式單例類中全部代碼訪問路徑都必須先得到類鎖的弊端。

相關文章
相關標籤/搜索