單例模式
特點:全局唯一,在整個程序中,只有一個對象。
什麼樣的類適合單例?
- 全局使用的類
- 創建和銷燬會消耗很多系統資源的類
- 數據庫連接池
- 工廠類
- 數據源
應用:
- Spring的Bean默認情況下是單例
- 項目中,讀取配置文件的類,一般也只有一個對象。沒有必要每次使用配置文件數據,每次new一個對象去讀取。
- 應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只能有一個實例去操作,否則內容不好追加。
- 數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源。
- 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。
- Application 也是單例的典型應用(Servlet編程中會涉及到)
- 在servlet編程中,每個Servlet也是單例
- 在spring MVC框架/struts1框架中,控制器對象也是單例
餓漢式
優點:
- 類一加載就創建實例,沒有延遲
- 線程安全
缺點:
- 反射和反序列化會破壞單例
- 如果沒有用到該類就會產生內存浪費
- 如果加載的資源很大,程序啓動的時候就會產生效率問題
class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
反射破壞單例:
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Singleton instance = Singleton.getInstance();
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
System.out.println(instance == instance1); // false
}
反序列化破壞單例:
@Test
public void test() throws IOException, ClassNotFoundException {
Singleton i1 = Singleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./singleton.obj"));
out.writeObject(i1);
out.close();
FileInputStream in = new FileInputStream("./singleton.obj");
ObjectInputStream ois = new ObjectInputStream(in);
Singleton i2 = (Singleton) ois.readObject();
System.out.println(i1);
System.out.println(i2);
// com.qianyu.thread.Singleton@6842775d
// com.qianyu.thread.Singleton@1ae369b7
}
加入readResolve方法解決反序列化問題
import java.io.*;
class Singleton implements Serializable {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
爲了防止反射破壞單例,我們可以在構造器中添加判斷
import java.io.*;
class Singleton implements Serializable {
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("不能通過反射創建對象");
}
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
懶漢式
特點:
- 延時加載
- 線程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
模擬線程不安全情況:
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> System.out.println(Singleton.getInstance())).start();
}
Thread.sleep(1000);
}
使用同步解決線程不安全問題
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
雙重檢測鎖模式
懶漢式的單例模式雖然可以使用synchronized
解決線程不安全問題,但是每次獲取單例對象的時候都要執行synchronized
代碼塊,造成效率低,爲了解決這個問題,提出雙重檢測鎖模式
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
指令重排問題
instance = new Singleton()會執行如下操作:
- 分配內存空間
- 初始化對象
- instance執行(1)中分配的空間
但是在某些編譯器上,可能會發生指令重排:
- 分配內存空間
- instance執行(1)中分配的空間(但此時對象沒有初始化)
- 初始化對象
這樣的話,如果多個線程同時訪問的話,有可能會出現某些對象爲空的情況。爲了防止JVM進行指令重排,我們可以加上volatile關鍵字
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
登記式
登記式,也可以叫做“靜態內部類”式。
首先我們要知道:
- 只有調用內部類的時候,內部類纔開始加載(延時加載)
- 非靜態內部類要依附於外部類,也就是說外部類必須創建對象才能使用非靜態內部類
- 靜態內部類不用外部類創建對象就可以使用
class Test {
class Inner {
}
static class SInner {
}
}
public class Demo {
public static void main(String[] args) {
Test.Inner inner = new Test().new Inner();
Test.SInner s = new Test.SInner();
}
}
登記式相對於餓漢式的好處:
- 可以實現延遲加載
- 可以通過改造,使其對反射安全
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
// 如果使用反射調用會報錯
private Singleton() {
if (SingletonHolder.instance != null) {
throw new IllegalStateException();
}
}
public Singleton getInstance() {
return SingletonHolder.instance;
}
}
枚舉式
特點:
- java1.5之後出現
- 目前推薦實現單例的最佳方式
- 線程安全
- 立即初始化
- 自動支持序列化,防止反序列化創建新的對象
- 防止反射攻擊
public enum Singleton {
INSTANCE{
@Override
protected void dosomething() {
System.out.println("Singleton.dosomething");
}
};
protected abstract void dosomething();
}
反射調用構造器,會拋出異常:
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<Singleton> clazz = Singleton.class;
Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
// java.lang.NoSuchMethodException: com.qianyu.thread.Singleton.<init>()
// at java.lang.Class.getConstructor0(Class.java:3082)
// at java.lang.Class.getDeclaredConstructor(Class.java:2178)
// at com.qianyu.thread.Application.test(Application.java:11)
}
ThreadLocal
優點:
- 空間換時間
- 延時加載
缺點:
- 只能是在同一個線程中獲得的兩個對象纔是單例
代碼實現:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
private static ThreadLocal<Singleton> threadLocalSingleton = new ThreadLocal<Singleton>() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
public static Singleton getInstance() {
return threadLocalSingleton.get();
}
}
多線程環境模擬
@Test
public void test() throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println("s2 = " + s2 + " , s1 = " + s1);
}).start();
}
Thread.sleep(1000);
}
CAS
缺點:
- 可能會產生垃圾對象
代碼實現
import java.util.concurrent.atomic.*;
public class Singleton {
private static final AtomicReference<Singleton> instance = new AtomicReference<>();
private Singleton() {
}
public static final Singleton getInstance() {
while (true) {
Singleton current = instance.get();
if (current != null) {
return current;
}
current = new Singleton();
if (instance.compareAndSet(null, current)) {
return current;
}
}
}
}
總結
單例實現要點:
- 私有構造器
- 持有該類的屬性
- 對外提供獲取實例的靜態方法
上述幾種單例模式的比較
- 餓漢式:線程安全、反射不安全、反序列化不安全、非延時加載
- 懶漢式:線程不安全、反射不安全、反序列化不安全、延時加載
- 雙重檢測鎖:線程安全、反射不安全、反序列化不安全、延時加載
- 登記時:線程安全、反射安全、反序列化不安全、延時加載
- 枚舉式:線程安全、反射安全、反序列化安全、非延時加載
- ThreadLocal:不加鎖,以空間換時間,爲每個線程提供獨立副本,可以保證各自線程是單例的,但是不同線程之間不是單例的
- CAS:無鎖樂觀策略,線程安全