文章目錄
單例的實現方法總結
以下的內容不涉及基礎,比如什麼是單例?JVM類加載順序?等等。
僅僅是對所有單例的實現方法進行彙總。
一、最經典的餓漢模式實現方式
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
另外一個變種的實現方法,是將靜態成員改爲靜態代碼塊
public class Singleton1_2 {
private static Singleton1_2 instance;
static {
instance = new Singleton1_2();
}
private Singleton1_2 (){}
public static Singleton1_2 getInstance() {
return instance;
}
}
不管怎麼寫,本質上利用的都是“類的初始化過程(包含靜態成員賦值,以及靜態代碼塊的執行),只在類被加載到內存時執行一次”這一特性。
- 由於其原理,天然就是線程安全的
- 結構簡單理解容易
- 相對於懶漢模式,餓漢模式最麻煩的地方在於,如果創建單例類的對象要依賴參數或者外部配置文件的話,也就是說,業務場景需要在調用getInstance方法時傳入參數,決定用何種方式創建單例實例的話,餓漢模式就無法使用了。
二、懶漢模式實現方法
public class Singleton2 {
private static Singleton2 instance;
private Singleton2 (){
}
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
- 在必須使用延遲加載的場景下,替代餓漢模式
- 最重要的一點,爲了確保線程安全,必須使用synchronized關鍵字進行同步,影響性能。
三、雙重檢查方法
雙重檢查其實就是對於懶漢模式的一種性能改進,減小了synchronized關鍵字鎖定的代碼塊範圍。
第二重檢查的作用是:防止有別的線程,在第一重檢查和拿鎖之間創建了單例實例。
public class Singleton3 {
private volatile static Singleton3 instance;
private Singleton3 (){
}
public static Singleton3 getSingleton() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
四、靜態內部類方法
這種方法,也是利用了類加載的特性,在getInstance()方法調用靜態內部類的靜態成員變量時,靜態內部類SingletonHolder纔會被初始化,創建單例實例。
(複習:使用 Class.staticMember 方式引用類的靜態成員變量,屬於對類進行主動引用,在這種情況下會觸發類加載的初始化過程)
public class Singleton4 {
private static class SingletonHolder {
private static final Singleton4 INSTANCE = new Singleton4();
}
private Singleton4 (){
}
public static Singleton4 getInstance() {
return SingletonHolder.INSTANCE;
}
}
優點
- 延遲加載
- 無鎖,沒有性能損耗
- 天然線程安全
五、枚舉類
這是一種最簡潔但是最難理解的單例實現方法。但是《Effective Java》評價這是實現單例的最佳方法(參看該書第3條)
public enum Singleton5 {
/**
* 枚舉實現單例
*/
INSTANCE;
public void businessMethod() {
}
}
其調用方法如下:
Singleton5.INSTANCE.businessMethod()
下面解釋枚舉類爲什麼能實現單例。
因爲enum只是一個關鍵字,不是超類或者其他能看到源碼的東西。因此利用反編譯的手段來確認內部實現(利用jad)。
package singleton;
public final class Singleton5 extends Enum
{
public static Singleton5[] values()
{
return (Singleton5[])$VALUES.clone();
}
public static Singleton5 valueOf(String name)
{
return (Singleton5)Enum.valueOf(singleton/Singleton5, name);
}
private Singleton5(String s, int i)
{
super(s, i);
}
public void businessMethod()
{
}
public static final Singleton5 INSTANCE;
private static final Singleton5 $VALUES[];
static
{
INSTANCE = new Singleton5("INSTANCE", 0);
$VALUES = (new Singleton5[] {
INSTANCE
});
}
}
可以看到,使用enum關鍵字的話,實際會生成一個繼承了Enum,並且final的類。
public final class Singleton5 extends Enum
注意下面的這一段:
public static final Singleton5 INSTANCE;
private static final Singleton5 $VALUES[];
static
{
INSTANCE = new Singleton5("INSTANCE", 0);
$VALUES = (new Singleton5[] {
INSTANCE
});
}
- 根據類加載過程,在“鏈接”的“準備”階段,靜態且final的靜態成員INSTANCE被加載到了方法區(如果這裏有賦值操作的話就有值了,不會等到初始化階段,這是final與其他不同的地方)。
- 到了初始化階段,會執行靜態代碼塊內的內容,開闢內存空間存放單例實例,並將地址賦值給INSTANCE。
- 類加載過程是隻會執行一次的,所以本質上還是利用jvm規定的類加載過程,形成了天然的線程安全
- 另外,構造函數 new Singleton5(“INSTANCE”, 0) 實際上調用是 super,也就是 Enum 類的構造函數,第一個參數是枚舉名稱(name),第二個參數是順序(ordinal)。
- 前面除了枚舉類以外的單例的實現方法,都有一個弱點,如果需要進行序列化的話(implements Serializable),那麼在反序列化的時候,每次調用readObject()方法都會生成一個不同於原單例的新實例,單例失效。
- 對於枚舉,爲了保證枚舉類型符合Java相關規範(JSR),每一個枚舉類及其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java有特殊處理
- 序列化的時候,Java僅將枚舉類的name屬性輸出到結果中,反序列化的時候通過Enum的valueOf方法來根據名字查找枚舉對象。
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
- 而valueOf()調用enumConstantDirectory(),繼而調用getEnumConstantsShared(),可以看到裏面實際調用的是反編譯出來的那段代碼裏的value()方法,使用的實際上就是那個$VALUES[]。
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
T[] getEnumConstantsShared() {
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex) { return null; }
}
return enumConstants;
}
- 所以枚舉即使被反序列化也不會創建對象。
- 所以枚舉即使被反序列化也不會創建對象。
- 所以枚舉即使被反序列化也不會創建對象。
六、應對多個類加載器
前面的所有方法都有一個共通的問題:被多個類加載器加載。
這問題不算是鑽牛角尖,一些熱啓動機制的框架,就是利用多個類加載器實現的,這時候確實有可能造成單例變成多例。
在網上找到了一段代碼來解決這個問題,就是增加下面這個私有靜態類。
原理是在被調用getClass方法時,直接利用自身原來的類加載器進行類加載,確保自始至終一直是同一個類加載器在加載單例類。
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}