單例是最常見(jiàn)的設(shè)計(jì)模式之一,實(shí)現(xiàn)的方式非常多,同時(shí)需要注意的問(wèn)題也非常多。
本文主要內(nèi)容:
- 介紹單例模式
- 介紹單例模式的N中寫法
- 單例模式的安全性
- 序列化攻擊
- 反射攻擊
- 單例模式總結(jié)
- 介紹單例模式的典型應(yīng)用
單例模式
單例模式(Singleton Pattern):確保某一個(gè)類只有一個(gè)實(shí)例,而且自行實(shí)例化并向整個(gè)系統(tǒng)提供這個(gè)實(shí)例,這個(gè)類稱為單例類,它提供全局訪問(wèn)的方法。單例模式是一種對(duì)象創(chuàng)建型模式。
單例模式有三個(gè)要點(diǎn):
- 構(gòu)造方法私有化;
- 實(shí)例化的變量引用私有化;
- 獲取實(shí)例的方法共有
角色
Singleton(單例):在單例類的內(nèi)部實(shí)現(xiàn)只生成一個(gè)實(shí)例,同時(shí)它提供一個(gè)靜態(tài)的 getInstance() 工廠方法,讓客戶可以訪問(wèn)它的唯一實(shí)例;為了防止在外部對(duì)其實(shí)例化,將其構(gòu)造函數(shù)設(shè)計(jì)為私有;在單例類內(nèi)部定義了一個(gè) Singleton 類型的靜態(tài)對(duì)象,作為外部共享的唯一實(shí)例。
單例模式的七種寫法
1、餓漢式
// 線程安全
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
優(yōu)點(diǎn):簡(jiǎn)單,使用時(shí)沒(méi)有延遲;在類裝載時(shí)就完成實(shí)例化,天生的線程安全
缺點(diǎn):沒(méi)有懶加載,啟動(dòng)較慢;如果從始至終都沒(méi)使用過(guò)這個(gè)實(shí)例,則會(huì)造成內(nèi)存的浪費(fèi)。
2、餓漢式變種
// 線程安全
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
將類實(shí)例化的過(guò)程放在了靜態(tài)代碼塊中,在類裝載的時(shí)執(zhí)行靜態(tài)代碼塊中的代碼,初始化類的實(shí)例。優(yōu)缺點(diǎn)同上。
3、懶漢式
// 線程不安全
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
優(yōu)點(diǎn):懶加載,啟動(dòng)速度快、如果從始至終都沒(méi)使用過(guò)這個(gè)實(shí)例,則不會(huì)初始化該實(shí)力,可節(jié)約資源
缺點(diǎn):多線程環(huán)境下線程不安全。if (singleton == null) 存在競(jìng)態(tài)條件,可能會(huì)有多個(gè)線程同時(shí)進(jìn)入 if 語(yǔ)句,導(dǎo)致產(chǎn)生多個(gè)實(shí)例
4、懶漢式變種
// 線程安全,效率低
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
優(yōu)點(diǎn):解決了上一種實(shí)現(xiàn)方式的線程不安全問(wèn)題
缺點(diǎn):synchronized 對(duì)整個(gè) getInstance() 方法都進(jìn)行了同步,每次只有一個(gè)線程能夠進(jìn)入該方法,并發(fā)性能極差
5、雙重檢查鎖
// 線程安全
public class Singleton {
// 注意:這里有 volatile 關(guān)鍵字修飾
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
優(yōu)點(diǎn):線程安全;延遲加載;效率較高。
由于 JVM 具有指令重排的特性,在多線程環(huán)境下可能出現(xiàn) singleton 已經(jīng)賦值但還沒(méi)初始化的情況,導(dǎo)致一個(gè)線程獲得還沒(méi)有初始化的實(shí)例。volatile 關(guān)鍵字的作用:
- 保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見(jiàn)性
- 禁止進(jìn)行指令重排序
6、靜態(tài)內(nèi)部類
// 線程安全
public class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
優(yōu)點(diǎn):避免了線程不安全,延遲加載,效率高。
靜態(tài)內(nèi)部類的方式利用了類裝載機(jī)制來(lái)保證線程安全,只有在第一次調(diào)用getInstance方法時(shí),才會(huì)裝載SingletonInstance內(nèi)部類,完成Singleton的實(shí)例化,所以也有懶加載的效果。
加入?yún)?shù) -verbose:class 可以查看類加載順序
$ javac Singleton.java
$ java -verbose:class Singleton
7、枚舉
// 線程安全
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
優(yōu)點(diǎn):通過(guò)JDK1.5中添加的枚舉來(lái)實(shí)現(xiàn)單例模式,寫法簡(jiǎn)單,且不僅能避免多線程同步問(wèn)題,而且還能防止反序列化重新創(chuàng)建新的對(duì)象。
單例模式的安全性
單例模式的目標(biāo)是,任何時(shí)候該類都只有唯一的一個(gè)對(duì)象。但是上面我們寫的大部分單例模式都存在漏洞,被攻擊時(shí)會(huì)產(chǎn)生多個(gè)對(duì)象,破壞了單例模式。
序列化攻擊
通過(guò)Java的序列化機(jī)制來(lái)攻擊單例模式
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(singleton); // 序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
結(jié)果
com.singleton.HungrySingleton@ed17bee
com.singleton.HungrySingleton@46f5f779
false
Java 序列化是如何攻擊單例模式的呢?我們需要先復(fù)習(xí)一下Java的序列化機(jī)制
Java 序列化機(jī)制
java.io.ObjectOutputStream 是Java實(shí)現(xiàn)序列化的關(guān)鍵類,它可以將一個(gè)對(duì)象轉(zhuǎn)換成二進(jìn)制流,然后可以通過(guò) ObjectInputStream 將二進(jìn)制流還原成對(duì)象。具體的序列化過(guò)程不是本文的重點(diǎn),在此僅列出幾個(gè)要點(diǎn)。
Java 序列化機(jī)制的要點(diǎn):
- 需要序列化的類必須實(shí)現(xiàn)
java.io.Serializable接口,否則會(huì)拋出NotSerializableException異常 - 若沒(méi)有顯示地聲明一個(gè)
serialVersionUID變量,Java序列化機(jī)制會(huì)根據(jù)編譯時(shí)的class自動(dòng)生成一個(gè)serialVersionUID作為序列化版本比較(驗(yàn)證一致性),如果檢測(cè)到反序列化后的類的serialVersionUID和對(duì)象二進(jìn)制流的serialVersionUID不同,則會(huì)拋出異常 - Java的序列化會(huì)將一個(gè)類包含的引用中所有的成員變量保存下來(lái)(深度復(fù)制),所以里面的引用類型必須也要實(shí)現(xiàn)
java.io.Serializable接口 - 當(dāng)某個(gè)字段被聲明為
transient后,默認(rèn)序列化機(jī)制就會(huì)忽略該字段,反序列化后自動(dòng)獲得0或者null值 - 靜態(tài)成員不參與序列化
- 每個(gè)類可以實(shí)現(xiàn)
readObject、writeObject方法實(shí)現(xiàn)自己的序列化策略,即使是transient修飾的成員變量也可以手動(dòng)調(diào)用ObjectOutputStream的writeInt等方法將這個(gè)成員變量序列化。 - 任何一個(gè)readObject方法,不管是顯式的還是默認(rèn)的,它都會(huì)返回一個(gè)新建的實(shí)例,這個(gè)新建的實(shí)例不同于該類初始化時(shí)創(chuàng)建的實(shí)例
- 每個(gè)類可以實(shí)現(xiàn)
private Object readResolve()方法,在調(diào)用readObject方法之后,如果存在readResolve方法則自動(dòng)調(diào)用該方法,readResolve將對(duì)readObject的結(jié)果進(jìn)行處理,而最終readResolve的處理結(jié)果將作為readObject的結(jié)果返回。readResolve的目的是保護(hù)性恢復(fù)對(duì)象,其最重要的應(yīng)用就是保護(hù)性恢復(fù)單例、枚舉類型的對(duì)象 -
Serializable接口是一個(gè)標(biāo)記接口,可自動(dòng)實(shí)現(xiàn)序列化,而Externalizable繼承自Serializable,它強(qiáng)制必須手動(dòng)實(shí)現(xiàn)序列化和反序列化算法,相對(duì)來(lái)說(shuō)更加高效
序列化破壞單例模式的解決方案
根據(jù)上面對(duì)Java序列化機(jī)制的復(fù)習(xí),我們可以自定義一個(gè) readResolve,在其中返回類的單例對(duì)象,替換掉 readObject 方法反序列化生成的對(duì)象,讓我們自己寫的單例模式實(shí)現(xiàn)保護(hù)性恢復(fù)對(duì)象
public class HungrySingleton implements Serializable {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
private Object readResolve() {
return instance;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton singleton = HungrySingleton.getInstance();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
System.out.println(singleton);
System.out.println(newSingleton);
System.out.println(singleton == newSingleton);
}
}
再次運(yùn)行
com.singleton.HungrySingleton@24273305
com.singleton.HungrySingleton@24273305
true
注意:自己實(shí)現(xiàn)的單例模式都需要避免被序列化破壞
反射攻擊
在單例模式中,構(gòu)造器都是私有的,而反射可以通過(guò)構(gòu)造器對(duì)象調(diào)用 setAccessible(true) 來(lái)獲得權(quán)限,這樣就可以創(chuàng)建多個(gè)對(duì)象,來(lái)破壞單例模式了
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySingleton instance = HungrySingleton.getInstance();
Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 獲得權(quán)限
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
輸出結(jié)果
com.singleton.HungrySingleton@3b192d32
com.singleton.HungrySingleton@16f65612
false
反射攻擊解決方案
反射是通過(guò)它的Class對(duì)象來(lái)調(diào)用構(gòu)造器創(chuàng)建新的對(duì)象,我們只需要在構(gòu)造器中檢測(cè)并拋出異常就可以達(dá)到目的了
private HungrySingleton() {
// instance 不為空,說(shuō)明單例對(duì)象已經(jīng)存在
if (instance != null) {
throw new RuntimeException("單例模式禁止反射調(diào)用!");
}
}
運(yùn)行結(jié)果
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.singleton.HungrySingleton.main(HungrySingleton.java:32)
Caused by: java.lang.RuntimeException: 單例模式禁止反射調(diào)用!
at com.singleton.HungrySingleton.<init>(HungrySingleton.java:20)
... 5 more
注意,上述方法針對(duì)餓漢式單例模式是有效的,但對(duì)懶漢式的單例模式是無(wú)效的,懶漢式的單例模式是無(wú)法避免反射攻擊的!
為什么對(duì)餓漢有效,對(duì)懶漢無(wú)效?因?yàn)轲I漢的初始化是在類加載的時(shí)候,反射一定是在餓漢初始化之后才能使用;而懶漢是在第一次調(diào)用 getInstance() 方法的時(shí)候才初始化,我們無(wú)法控制反射和懶漢初始化的先后順序,如果反射在前,不管反射創(chuàng)建了多少對(duì)象,instance都將一直為null,直到調(diào)用 getInstance()。
事實(shí)上,實(shí)現(xiàn)單例模式的唯一推薦方法,是使用枚舉類來(lái)實(shí)現(xiàn)。
為什么推薦使用枚舉單例
寫下我們的枚舉單例模式
package com.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum SerEnumSingleton implements Serializable {
INSTANCE; // 單例對(duì)象
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
private SerEnumSingleton() {
}
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE;
singleton1.setContent("枚舉單例序列化");
System.out.println("枚舉序列化前讀取其中的內(nèi)容:" + singleton1.getContent());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject();
ois.close();
System.out.println(singleton1 + "\n" + singleton2);
System.out.println("枚舉序列化后讀取其中的內(nèi)容:" + singleton2.getContent());
System.out.println("枚舉序列化前后兩個(gè)是否同一個(gè):" + (singleton1 == singleton2));
Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
SerEnumSingleton singleton3 = constructor.newInstance(); // 通過(guò)反射創(chuàng)建對(duì)象
System.out.println("反射后讀取其中的內(nèi)容:" + singleton3.getContent());
System.out.println("反射前后兩個(gè)是否同一個(gè):" + (singleton1 == singleton3));
}
}
運(yùn)行結(jié)果,序列化前后的對(duì)象是同一個(gè)對(duì)象,而反射的時(shí)候拋出了異常
枚舉序列化前讀取其中的內(nèi)容:枚舉單例序列化
INSTANCE
INSTANCE
枚舉序列化后讀取其中的內(nèi)容:枚舉單例序列化
枚舉序列化前后兩個(gè)是否同一個(gè):true
Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)
編譯后,再通過(guò) JAD 進(jìn)行反編譯得到下面的代碼
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: SerEnumSingleton.java
package com.singleton;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public final class SerEnumSingleton extends Enum
implements Serializable
{
public static SerEnumSingleton[] values()
{
return (SerEnumSingleton[])$VALUES.clone();
}
public static SerEnumSingleton valueOf(String name)
{
return (SerEnumSingleton)Enum.valueOf(com/singleton/SerEnumSingleton, name);
}
public String getContent()
{
return content;
}
public void setContent(String content)
{
this.content = content;
}
private SerEnumSingleton(String s, int i)
{
super(s, i);
}
public static void main(String args[])
throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException
{
SerEnumSingleton singleton1 = INSTANCE;
singleton1.setContent("\u679A\u4E3E\u5355\u4F8B\u5E8F\u5217\u5316");
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton1.getContent()).toString());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
oos.writeObject(singleton1);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
SerEnumSingleton singleton2 = (SerEnumSingleton)ois.readObject();
ois.close();
System.out.println((new StringBuilder()).append(singleton1).append("\n").append(singleton2).toString());
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton2.getContent()).toString());
System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton2).toString());
Constructor constructor = com/singleton/SerEnumSingleton.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
SerEnumSingleton singleton3 = (SerEnumSingleton)constructor.newInstance(new Object[0]);
System.out.println((new StringBuilder()).append("\u53CD\u5C04\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton3.getContent()).toString());
System.out.println((new StringBuilder()).append("\u53CD\u5C04\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton3).toString());
}
public static final SerEnumSingleton INSTANCE;
private String content;
private static final SerEnumSingleton $VALUES[];
static
{
INSTANCE = new SerEnumSingleton("INSTANCE", 0);
$VALUES = (new SerEnumSingleton[] {
INSTANCE
});
}
}
通過(guò)反編譯后代碼我們可以看到,ublic final class T extends Enum,說(shuō)明,當(dāng)我們使用enmu來(lái)定義一個(gè)枚舉類型的時(shí)候,編譯器會(huì)自動(dòng)幫我們創(chuàng)建一個(gè)final類型的類繼承Enum類,所以枚舉類型不能被繼承。
那么,為什么推薦使用枚舉單例呢?
1. 枚舉單例寫法簡(jiǎn)單
2. 線程安全&懶加載
代碼中 INSTANCE 變量被 public static final 修飾,因?yàn)閟tatic類型的屬性是在類加載之后初始化的,JVM可以保證線程安全;且Java類是在引用到的時(shí)候才進(jìn)行類加載,所以枚舉單例也有懶加載的效果。
3. 枚舉自己能避免序列化攻擊
為了保證枚舉類型像Java規(guī)范中所說(shuō)的那樣,每一個(gè)枚舉類型極其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規(guī)定。
在序列化的時(shí)候Java僅僅是將枚舉對(duì)象的name屬性輸出到結(jié)果中,反序列化的時(shí)候則是通過(guò)java.lang.Enum的valueOf方法來(lái)根據(jù)名字查找枚舉對(duì)象。同時(shí),編譯器是不允許任何對(duì)這種序列化機(jī)制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我們看一下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);
}
從代碼中可以看到,代碼會(huì)嘗試從調(diào)用enumType這個(gè)Class對(duì)象的enumConstantDirectory()方法返回的map中獲取名字為name的枚舉對(duì)象,如果不存在就會(huì)拋出異常。再進(jìn)一步跟到enumConstantDirectory()方法,就會(huì)發(fā)現(xiàn)到最后會(huì)以反射的方式調(diào)用enumType這個(gè)類型的values()靜態(tài)方法,也就是上面我們看到的編譯器為我們創(chuàng)建的那個(gè)方法,然后用返回結(jié)果填充enumType這個(gè)Class對(duì)象中的enumConstantDirectory屬性。所以,JVM對(duì)序列化有保證。
4. 枚舉能夠避免反射攻擊,因?yàn)榉瓷洳恢С謩?chuàng)建枚舉對(duì)象
Constructor類的 newInstance方法中會(huì)判斷是否為 enum,若是會(huì)拋出異常
@CallerSensitive
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// 不能為 ENUM,否則拋出異常:不能通過(guò)反射創(chuàng)建 enum 對(duì)象
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
單例模式總結(jié)
單例模式作為一種目標(biāo)明確、結(jié)構(gòu)簡(jiǎn)單、理解容易的設(shè)計(jì)模式,在軟件開(kāi)發(fā)中使用頻率相當(dāng)高,在很多應(yīng)用軟件和框架中都得以廣泛應(yīng)用。
單例模式的主要優(yōu)點(diǎn)
- 單例模式提供了對(duì)唯一實(shí)例的受控訪問(wèn)。
- 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對(duì)象,因此可以節(jié)約系統(tǒng)資源,對(duì)于一些需要頻繁創(chuàng)建和銷毀的對(duì)象,單例模式可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實(shí)例?;趩卫J轿覀兛梢赃M(jìn)行擴(kuò)展,使用與單例控制相似的方法來(lái)獲得指定個(gè)數(shù)的對(duì)象實(shí)例,既節(jié)省系統(tǒng)資源,又解決了單例單例對(duì)象共享過(guò)多有損性能的問(wèn)題。
單例模式的主要缺點(diǎn)
- 由于單例模式中沒(méi)有抽象層,因此單例類的擴(kuò)展有很大的困難。
- 單例類的職責(zé)過(guò)重,在一定程度上違背了 "單一職責(zé)原則"。
- 如果實(shí)例化的共享對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)可能會(huì)認(rèn)為它是垃圾,會(huì)自動(dòng)銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致共享的單例對(duì)象狀態(tài)的丟失。
適用場(chǎng)景
- 系統(tǒng)只需要一個(gè)實(shí)例對(duì)象,如系統(tǒng)要求提供一個(gè)唯一的序列號(hào)生成器或資源管理器,或者需要考慮資源消耗太大而只允許創(chuàng)建一個(gè)對(duì)象。
- 客戶調(diào)用類的單個(gè)實(shí)例只允許使用一個(gè)公共訪問(wèn)點(diǎn),除了該公共訪問(wèn)點(diǎn),不能通過(guò)其他途徑訪問(wèn)該實(shí)例。
單例模式的典型應(yīng)用
JDK Runtime 餓漢單例
JDK Runtime類代表著Java程序的運(yùn)行時(shí)環(huán)境,每個(gè)Java程序都有一個(gè)Runtime實(shí)例,該類會(huì)被自動(dòng)創(chuàng)建,我們可以通過(guò) Runtime.getRuntime() 方法來(lái)獲取當(dāng)前程序的Runtime實(shí)例。一旦得到了一個(gè)當(dāng)前的Runtime對(duì)象的引用,就可以調(diào)用Runtime對(duì)象的方法去控制Java虛擬機(jī)的狀態(tài)和行為。
Runtime 應(yīng)用了餓漢式單例模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {
}
//....
}
API 介紹
addShutdownHook(Thread hook) 注冊(cè)新的虛擬機(jī)來(lái)關(guān)閉掛鉤。
availableProcessors() 向 Java 虛擬機(jī)返回可用處理器的數(shù)目。
exec(String command) 在單獨(dú)的進(jìn)程中執(zhí)行指定的字符串命令。
exec(String[] cmdarray) 在單獨(dú)的進(jìn)程中執(zhí)行指定命令和變量。
exec(String[] cmdarray, String[] envp) 在指定環(huán)境的獨(dú)立進(jìn)程中執(zhí)行指定命令和變量。
exec(String[] cmdarray, String[] envp, File dir) 在指定環(huán)境和工作目錄的獨(dú)立進(jìn)程中執(zhí)行指定的命令和變量。
exec(String command, String[] envp) 在指定環(huán)境的單獨(dú)進(jìn)程中執(zhí)行指定的字符串命令。
exec(String command, String[] envp, File dir) 在有指定環(huán)境和工作目錄的獨(dú)立進(jìn)程中執(zhí)行指定的字符串命令。
exit(int status) 通過(guò)啟動(dòng)虛擬機(jī)的關(guān)閉序列,終止當(dāng)前正在運(yùn)行的 Java 虛擬機(jī)。
freeMemory() 返回 Java 虛擬機(jī)中的空閑內(nèi)存量。
gc() 運(yùn)行垃圾回收器。
getRuntime() 返回與當(dāng)前 Java 應(yīng)用程序相關(guān)的運(yùn)行時(shí)對(duì)象。
halt(int status) 強(qiáng)行終止目前正在運(yùn)行的 Java 虛擬機(jī)。
load(String filename) 加載作為動(dòng)態(tài)庫(kù)的指定文件名。
loadLibrary(String libname) 加載具有指定庫(kù)名的動(dòng)態(tài)庫(kù)。
maxMemory() 返回 Java 虛擬機(jī)試圖使用的最大內(nèi)存量。
removeShutdownHook(Thread hook) 取消注冊(cè)某個(gè)先前已注冊(cè)的虛擬機(jī)關(guān)閉掛鉤。
runFinalization() 運(yùn)行掛起 finalization 的所有對(duì)象的終止方法。
totalMemory() 返回 Java 虛擬機(jī)中的內(nèi)存總量。
traceInstructions(on) 啟用/禁用指令跟蹤。
traceMethodCalls(on) 啟用/禁用方法調(diào)用跟蹤。
AWT Desktop 容器單例
Desktop 類允許 Java 應(yīng)用程序啟動(dòng)已在本機(jī)桌面上注冊(cè)的關(guān)聯(lián)應(yīng)用程序,以處理 URI 或文件。支持的操作包括:
- 打開(kāi)瀏覽器: 啟動(dòng)用戶默認(rèn)瀏覽器來(lái)顯示指定的 URI;
- 打開(kāi)郵件客戶端: 啟動(dòng)帶有可選 mailto URI 的用戶默認(rèn)郵件客戶端;
- 打開(kāi)文件/文件夾: 啟動(dòng)已注冊(cè)的應(yīng)用程序,以打開(kāi)、編輯 或 打印 指定的文件。
Desktop 通過(guò)一個(gè)容器來(lái)管理單例對(duì)象
public class Desktop {
// synchronized 同步方法
public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform");
}
sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class); // 獲取單例對(duì)象
// 存在則返回,不存在則創(chuàng)建,創(chuàng)建后put進(jìn)容器
if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}
return desktop;
}
AppContext 中有一個(gè) HashMap 對(duì)象table,是實(shí)際的容器對(duì)象
private final Map<Object, Object> table = new HashMap();
spring AbstractFactoryBean
AbstractFactoryBean 類
public final T getObject() throws Exception {
if (this.isSingleton()) {
return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance();
} else {
return this.createInstance();
}
}
private T getEarlySingletonInstance() throws Exception {
Class<?>[] ifcs = this.getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
} else {
if (this.earlySingletonInstance == null) {
// 通過(guò)代理創(chuàng)建對(duì)象
this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler());
}
return this.earlySingletonInstance;
}
}
Mybatis ErrorContext ThreadLocal
ErrorContext 類,通過(guò) ThreadLocal 管理單例對(duì)象,一個(gè)線程一個(gè)ErrorContext對(duì)象,ThreadLocal可以保證線程安全
public class ErrorContext {
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
private ErrorContext() {
}
public static ErrorContext instance() {
ErrorContext context = LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
//...
}
參考:
http://www.hollischuang.com/archives/197
https://www.cnblogs.com/chiclee/p/9097772.html
https://blog.csdn.net/abc123lzf/article/details/82318148
后記
歡迎評(píng)論、轉(zhuǎn)發(fā)、分享,您的支持是我最大的動(dòng)力
更多內(nèi)容可訪問(wèn)我的個(gè)人博客:http://laijianfeng.org
關(guān)注【小旋鋒】微信公眾號(hào),及時(shí)接收博文推送
