創(chuàng)建型模式——單例模式(四)

該項目源碼地址:https://github.com/ggb2312/JavaNotes/tree/master/design-pattern(設計模式相關代碼與筆記)

1. 定義

保證一個類僅有一個實例愿卒,并提供一個全局訪問點

2. 介紹

適用場景
想確保任何情況下都絕對只有一個實例

單例模式的重點

  • 私有構造器
  • 線程安全
  • 延遲加載
  • 序列化和反序列化安全
  • 反射攻擊

3. 模式實例

在Java中,我們通過使用對象(類實例化后)來操作這些類铝阐,類實例化是通過它的構造方法進行的,要是想實現(xiàn)一個類只有一個實例化對象父腕,就要對類的構造方法下功夫叼架。

類圖

單例模式的一般實現(xiàn):(含使用步驟)

public class Singleton {
//1. 創(chuàng)建私有變量 ourInstance(用以記錄 Singleton 的唯一實例)
//2. 內(nèi)部進行實例化
    private static Singleton ourInstance  = new  Singleton();

//3. 把類的構造方法私有化,不讓外部調(diào)用構造方法實例化
    private Singleton() {
    }
//4. 定義公有方法提供該類的全局唯一訪問點
//5. 外部通過調(diào)用getInstance()方法來返回唯一的實例
    public static  Singleton newInstance() {
        return ourInstance;
    }
}

3.1 懶漢式(延遲加載)

懶漢式基礎實現(xiàn)

特點:懶加載套像,需要時才創(chuàng)建酿联,線程不安全

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){

    }
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

測試

public class Test {
    public static void main(String[] args){
        LazySingleton lazySingleton1 = LazySingleton.getInstance();
        LazySingleton lazySingleton2 = LazySingleton.getInstance();
        System.out.println(lazySingleton1);
        System.out.println(lazySingleton1);
        System.out.println(lazySingleton1 == lazySingleton2);
    }
}
測試結果

懶漢式是線程不安全的,假如有兩個線程使用懶漢式創(chuàng)建對象凉夯,thread1調(diào)用getInstance()方法時货葬,lazySingleton == null為true,進入if劲够,但未new對象震桶。此時cpu調(diào)度,thread2調(diào)用getInstance()方法時征绎,lazySingleton == null為true蹲姐,進入if,并new了對象人柿,返回給thread2柴墩。此時thread1開始在if里面new對象,返回給thread1.創(chuàng)建了兩次對象凫岖。

懶漢式多線程創(chuàng)建對象測試

public class T implements Runnable {
    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+"  " + lazySingleton);
    }
}

修改測試類

public class Test {
    public static void main(String[] args){
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("end");
    }
}

在多線程debug江咳,人為干擾的情況下(或者多run幾次也可以),創(chuàng)建了兩個不同的對象哥放。

測試結果

3.1.1 同步鎖

特點:使用同步鎖歼指,線程安全,但性能比較差
修改LazySingleton單例類(靜態(tài)方法synchronized會鎖住這個文件)

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){

    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

在多線程debug甥雕,人為干擾的情況下踩身,同步鎖會保證只有一個線程進入同步方法,創(chuàng)建對象社露。

3.1.2 double-checked locking(雙重檢查加鎖)

特點:懶加載挟阻,jdk1.5及以上版本線程安全,性能好
創(chuàng)建LazyDoubleCheckSingleton類

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

在代碼的第12行首先判斷l(xiāng)azyDoubleCheckSingleton是否為null(是否分配內(nèi)存地址),如果lazyDoubleCheckSingleton為null使用synchronized同步鎖保證線程安全附鸽,將同步鎖放在if判斷內(nèi)比直接放在方法上脱拼,大大減少了性能開銷。

我們來模擬一下多線程情況下拒炎。

thread1與thread2都進入了12行iflazyDoubleCheckSingleton == null判斷為true挪拟,進入if。thread1握住了鎖進入了同步代碼塊击你,thread2阻塞玉组。thread1進入14行再次iflazyDoubleCheckSingleton == null判斷為true,進入15行new對象丁侄,釋放同步鎖惯雳,return對象。thread2握住了鎖進入了同步代碼塊鸿摇,iflazyDoubleCheckSingleton == null判斷為false石景,釋放鎖,直接return對象拙吉。

看似沒有任何問題潮孽,實際上會出現(xiàn)問題的,問題出在第12行和第15行筷黔,分析如下:

我們通常會將第15行l(wèi)azyDoubleCheckSingleton = new LazyDoubleCheckSingleton();看成是一個步驟往史,實際上JVM內(nèi)部已經(jīng)轉換為三條指令。

三條指令如下:

步驟一: memory = allocate();——》分配對象的內(nèi)存空間
步驟二: ctorInstance(memory);——》初始化對象
步驟三: instance = memory;——》設置lazyDoubleCheckSingleton 指向剛分配的內(nèi)存地址

對象創(chuàng)建圖示:

java對象創(chuàng)建過程

在這里會出現(xiàn)一個指令重排的問題佛舱。

指令重排:大多數(shù)現(xiàn)代微處理器都會采用將指令亂序執(zhí)行(out-of-order execution椎例,簡稱OoOE或OOE)的方法,在條件允許的情況下请祖,直接運行當前有能力立即執(zhí)行的后續(xù)指令订歪,避開獲取下一條指令所需數(shù)據(jù)時造成的等待。通過亂序執(zhí)行的技術肆捕,處理器可以大大提高執(zhí)行效率刷晋。
除了處理器,常見的Java運行時環(huán)境的JIT編譯器也會做指令重排序操作慎陵,即生成的機器指令與字節(jié)碼指令順序不一致掏秩。

經(jīng)過重排序后的對象創(chuàng)建過程如下:

步驟一: memory = allocate();——》分配對象的內(nèi)存空間
步驟三: instance = memory; ——》設置lazyDoubleCheckSingleton 指向剛分配的內(nèi)存地址
步驟二: ctorInstance(memory);——》初始化對象

經(jīng)過重排序后的對象創(chuàng)建過程圖示如下:

指令重排后的對象創(chuàng)建

在單線程指令重排的情況下,由于“intra-thread semantics”的存在荆姆,保證指令重排序不會改變單線程內(nèi)的程序執(zhí)行結果。

在多線程指令重排的情況下映凳,thread1進入了12行lazyDoubleCheckSingleton == null判斷為true胆筒,進入if。thread1握住了鎖進入了同步代碼塊。thread1進入14行再次lazyDoubleCheckSingleton == null判斷為true仆救,進入15行new對象抒和,在new對象的過程中:1.分配對象的內(nèi)存空間 3.設置lazyDoubleCheckSingleton 指向剛分配的內(nèi)存地址。此時thread2調(diào)用getInstance()彤蔽,進入了12行l(wèi)azyDoubleCheckSingleton == null判斷為false(ps:java的“==”比的內(nèi)存地址摧莽,此時lazyDoubleCheckSingleton已經(jīng)分配內(nèi)存地址了),直接返回現(xiàn)有的對象lazyDoubleCheckSingleton顿痪,thread2使用lazyDoubleCheckSingleton時就會出錯镊辕,拋異常,因為lazyDoubleCheckSingleton并未被初始化蚁袭。

多線程指令重排

上面說的那么多征懈,大家估計會暈,總結一下原因:thread1在第15行執(zhí)行“1.分配對象的內(nèi)存空間地址揩悄、3.設置instance指向內(nèi)存空間地址”時卖哎,thread2在第12行判斷instance是否為null,由于thread1設置了instance的內(nèi)存空間地址删性,所以返回false亏娜,直接返回instance,thread2就會直接拿著instance去使用蹬挺,instance沒有被初始化就會報錯维贺。

歸根究底,是因為thread1指令重排過程汗侵,thread2使用了未初始化的對象幸缕。

我們知道了問題所在,就可以從兩方面入手晰韵。
方法1.不允許thread1第二步與第三步指令重排发乔。
方法2.thread1指令重排時,不讓thread2看到這個指令重排雪猪。

使用volatile關鍵字是使用方法1 不允許thread1第二步與第三步指令重排栏尚。

關于volatile:
在多線程情況下,cpu會有共享內(nèi)存只恨,在加入volatile關鍵字后译仗,所有線程都可以看到共享內(nèi)存的最新狀態(tài),保證內(nèi)存的可見性官觅。
用volatile關鍵字修飾的共享變量纵菌,在進行寫操作時,會多出一些匯編代碼休涤,主要作用:會將當前處理器的緩存行的數(shù)據(jù)寫到系統(tǒng)內(nèi)存中毡泻,這個寫回內(nèi)存的操作,會使其他處理器緩存的數(shù)據(jù)失效纪吮,由于處理器緩存的數(shù)據(jù)失效了虑稼,它們就會從共享內(nèi)存同步數(shù)據(jù),這樣就保證了內(nèi)存的可見性(緩存一致性協(xié)議)。

使用volatile關鍵字重寫LazyDoubleCheckSingleton類

package com.desgin.pattern.creational.singleton;
/**
 * Create by lastwhisper on 2019/1/25
 */
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){

    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

修改多線程T類

package com.desgin.pattern.creational.singleton;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class T implements Runnable {
    @Override
    public void run() {
        LazyDoubleCheckSingleton lzyDoubleCheckSingleton = LazyDoubleCheckSingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+"  " + lzyDoubleCheckSingleton);
    }
}

測試類

package com.desgin.pattern.creational.singleton;
/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args){
            Thread t1 = new Thread(new T());
            Thread t2 = new Thread(new T());
            t1.start();
            t2.start();
            System.out.println("end");
    }
}

測試結果:

測試結果

3.1.3 靜態(tài)內(nèi)部類

特點:不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化围来。
使用靜態(tài)內(nèi)部類是使用方法2:thread1指令重排時,不讓thread2看到這個指令重排(ps:因為jvm會使用初始化鎖保證多個線程下只會有一個線程加載類)匈睁。

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {
    }
    private static class InnerClass{
        private static  StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
}

那么监透,靜態(tài)內(nèi)部類又是如何實現(xiàn)線程安全的呢?
首先软舌,我們先了解下類的加載時機才漆。有5種情況,首次發(fā)生時佛点,一個類將被立刻初始化醇滥,類是泛指,包括接口超营。

1.有一個類的實例被創(chuàng)建
2.類中聲明的靜態(tài)方法被調(diào)用
3.類中聲明的靜態(tài)成員被賦值
4.類中聲明的靜態(tài)成員被使用鸳玩,且不是常量成員
5.類是頂級類,且類中有嵌套的斷言語句

我們這里使用的是4.類中聲明的靜態(tài)成員被使用演闭,且不是常量成員不跟。

JVM在類的初始化階段(也就是class被加載后,被線程使用前米碰,都是類的初始化階段)窝革,JVM會保證一個類的<clinit>()方法在多線程環(huán)境中被正確地加鎖、同步吕座,如果多個線程同時去初始化一個類虐译,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法,其他線程都需要阻塞等待吴趴,直到活動線程執(zhí)行<clinit>()方法完畢漆诽。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是锣枝,其他線程雖然會被阻塞厢拭,但如果執(zhí)行<clinit>()方法后,其他線程喚醒之后不會再次進入<clinit>()方法撇叁。同一個加載器下供鸠,一個類型只會初始化一次。)陨闹,在實際應用中回季,這種阻塞往往是很隱蔽的家制。
ps:<clinit>()是用于初始化靜態(tài)的類變量, <init>()是初始化實例變量

jvm初始化鎖圖示

簡單來說:在執(zhí)行類的初始化期間泡一,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化

修改多線程T類

public class T implements Runnable {
    @Override
    public void run() {
        StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
       System.out.println(Thread.currentThread().getName() + "  " + instance);
    }
}

測試結果:

測試結果

3.2 餓漢式(立即加載)

餓漢式

特點:實現(xiàn)簡單觅廓,由于是立即加載鼻忠,如果這個類一直不被使用就會浪費內(nèi)存。

public class HungrySingleton {
    private static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

簡單來說杈绸,在類的初始化期間帖蔓,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化瞳脓,保證線程安全(ps:詳細解釋在3.1.3 靜態(tài)內(nèi)部類)

4. 序列化破壞解決方案及原理分析

使用上述的任意一個正確的單例模式進行序列化破壞測試都可以塑娇,這里我們選擇餓漢式進行測試。

4.1 序列化破壞

為HungrySingleton類實現(xiàn)Serializable接口進行序列化

public class HungrySingleton implements Serializable {
    private static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

測試代碼

import java.io.*;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args) throws Exception {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
        oos.writeObject(instance);

        File file = new File("E:\\singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton)ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果劫侧,發(fā)現(xiàn)單例生成的對象與序列化后反序列化回來的對象不一樣了埋酬。

序列化攻擊測試

我們?yōu)镠ungrySingleton類添加一個readResolve()方法

public class HungrySingleton implements Serializable {
    private static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {

    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    private Object readResolve(){
        return hungrySingleton;
    }

}

測試代碼不變,再次測試烧栋,發(fā)現(xiàn)單例生成的對象與序列化后反序列化回來的對象一樣了写妥。

序列化攻擊測試

4.2 返回不同對象原理分析

在測試代碼Test的反序列化方法readObject()里

readObject()

readObject()調(diào)用readObject0(false);

readObject0(false)

在readObject0()里。會進入一個switch审姓,調(diào)用checkResolve(readOrdinaryObject(unshared))

checkResolve(readOrdinaryObject(unshared))

在readOrdinaryObject()方法里面調(diào)用
obj = desc.isInstantiable() ? desc.newInstance() : null;

obj = desc.isInstantiable() ? desc.newInstance() : null

desc.isInstantiable()珍特,只要實現(xiàn)serializable/externalizable接口就返回true。
返回true就會執(zhí)行desc.newInstance()魔吐,obj就會被newInstance()初始化扎筒,所以序列化后返回的對象與單例獲得對象地址不同

desc.isInstantiable()

4.3 返回相同對象原理分析

既然實現(xiàn)了serializable/externalizable接口,反序列化時就會重新創(chuàng)建對象酬姆,造成單例模式創(chuàng)建出不同的對象嗜桌,為什么加上readResolve()方法就可以單例了呢?

private Object readResolve(){
    return hungrySingleton;
}

接著readOrdinaryObject()方法

readOrdinaryObject()

desc.hasReadResolveMethod()方法轴踱,對于實現(xiàn)了serializable or externalizable接口症脂,同時有readResolve方法的,返回true淫僻。進入if判斷诱篷,執(zhí)行Object rep = desc.invokeReadResolve(obj);

desc.hasReadResolveMethod()

Object rep = desc.invokeReadResolve(obj);中,由于我們有readResolve()方法雳灵,會直接執(zhí)行readResolveMethod.invoke(obj, (Object[]) null);棕所,然后invoke執(zhí)行我們單例模式本身的readResolve()方法,直接返回hungrySingleton悯辙。

readResolveMethod.invoke(obj, (Object[]) null)
readResolveMethod
readResolve()

所以添加readResolve方法琳省,返回了相同對象迎吵。

4.4 總結

使用序列化時,進行反序列化會使用反射重新創(chuàng)建對象针贬,解決方案就是添加readResolve方法击费,但是添加readResolve方法,也只是給反射創(chuàng)建的對象覆蓋成單例創(chuàng)建的對象桦他,在單例模式使用序列化時一定要注意蔫巩。

5. 反射攻擊解決方案及原理分析

反射攻擊就是,通過反射創(chuàng)建與單例對象不同的對象快压,破壞單例模式圆仔。
雖然在單例模式構造器是私有的,但是我們可以通過反射進行修改權限蔫劣,進行訪問坪郭。

5.1 反射攻擊

(1)餓漢式

使用反射破壞餓漢式單例模式HungrySingleton,編寫測試代碼

public class Test {
    public static void main(String[] args) throws Exception {
        /*反射測試*/
        Class objectClass  = HungrySingleton.class;
        
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
       
         System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果脉幢,可以通過反射破壞單例模式餓漢式的對象創(chuàng)建

反射破壞單例模式餓漢式測試結果

(2)懶漢式之靜態(tài)內(nèi)部類

靜態(tài)內(nèi)部類

public class Test {
   public static void main(String[] args) throws Exception {
       /*反射測試*/
       Class objectClass  = StaticInnerClassSingleton.class;

       Constructor constructor = objectClass.getDeclaredConstructor();
       constructor.setAccessible(true);
       StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
       StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();

       System.out.println(instance);
       System.out.println(newInstance);
       System.out.println(instance == newInstance);
   }
}

測試結果歪沃,單例模式懶漢式的靜態(tài)內(nèi)部類實現(xiàn),也可以通過反射破壞鸵隧。

單例模式的靜態(tài)內(nèi)部類反射攻擊

(3)雙重檢測加鎖與懶漢式同步鎖

代碼與(1)(2)攻擊方式類似绸罗,不在贅述。

5.2 解決方案

反射是通過修改私有構造器的訪問權限豆瘫,破壞單例模式的珊蟀。我們可以在私有構造器進行一些判斷,防止反射修改訪問權限外驱,調(diào)用私有構造器初始化對象育灸。

ps:此方式只能防止 類加載時創(chuàng)建單例對象的方式

**(1) 餓漢式 **

在私有構造器中判斷是否存在已經(jīng)存在單例對象,如果存在就拋異常昵宇。

public class HungrySingleton implements Serializable {
       private static HungrySingleton hungrySingleton;
    static {
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("單例構造器禁止反射調(diào)用");
        }
    }
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

}

測試

import java.lang.reflect.Constructor;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args) throws Exception {
        /*反射測試*/
        Class objectClass = HungrySingleton.class;
        
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果磅崭,成功抵擋反射攻擊。

餓漢式抵擋反射攻擊

(2) 懶漢式之靜態(tài)內(nèi)部類

同樣的思路瓦哎,在私有構造器中判斷是否存在已經(jīng)存在單例對象砸喻,如果存在就拋異常。

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("單例構造器禁止反射調(diào)用");
        }
    }

    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
}

測試代碼

import java.lang.reflect.Constructor;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args) throws Exception {
        /*反射測試*/
        Class objectClass  = StaticInnerClassSingleton.class;

        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
        StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果蒋譬,成功抵擋反射攻擊割岛。

懶漢式之靜態(tài)內(nèi)部類抵擋反射攻擊

(3) 懶漢式之同步鎖與雙重檢測加鎖

很不幸,這兩種方式無法抵擋反射攻擊犯助,因為這兩種方式在類加載時并不創(chuàng)建對象癣漆。在私有構造器進行判斷的方法只能防止類加載時創(chuàng)建單例對象的方式。

這里我們以懶漢式之同步鎖為例(ps:雙重檢測鎖也相同)剂买。
在私有構造器中添加判斷

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton() {
        if (lazySingleton != null) {
            throw new RuntimeException("單例構造器禁止反射調(diào)用");
        }

    }

    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

測試代碼

import java.lang.reflect.Constructor;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args) throws Exception {
        /*反射測試*/
        Class objectClass = LazySingleton.class;

        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);

        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton newInstance = (LazySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果

懶漢式之同步鎖抵擋反射攻擊測試結果

看似抵擋了反射攻擊惠爽。
我們來交換一下測試代碼這兩行代碼執(zhí)行順序癌蓖。

LazySingleton newInstance = (LazySingleton) constructor.newInstance();
LazySingleton instance = LazySingleton.getInstance();

變換為:

LazySingleton instance = LazySingleton.getInstance();
LazySingleton newInstance = (LazySingleton) constructor.newInstance();

測試結果,無法阻止反射攻擊婚肆。

懶漢式之同步鎖抵擋反射攻擊測試結果

雙重檢測加鎖效果也類似租副,無法抵擋反射攻擊。

5.3 原理分析與擴展

(1)原理分析

我們接著“5.2.3 懶漢式之同步鎖與雙重檢測加鎖”旬痹,來分析下測試代碼附井。

Class objectClass = LazySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = (LazySingleton) constructor.newInstance();

這幾行代碼,會通過反射創(chuàng)建LazySingleton對象两残,但是靜態(tài)私有變量lazySingleton還是為null。

private static LazySingleton lazySingleton = null;

我們使用反射創(chuàng)建對象與getInstance()創(chuàng)建對象把跨,打印一下私有靜態(tài)變量lazySingleton(暫時將權限設為public人弓,測試一下私有靜態(tài)變量lazySingleton)

public static LazySingleton lazySingleton = null;
私有靜態(tài)變量lazySingleton

所以將反射創(chuàng)建對象代碼constructor.newInstance()放在LazySingleton.getInstance()之前,constructor.newInstance()創(chuàng)建LazySingleton對象的靜態(tài)私有變量lazySingleton為null着逐,LazySingleton.getInstance()創(chuàng)建對象調(diào)用私有構造器時if判斷失效崔赌。

如果是多線程情況下,thread1執(zhí)行constructor.newInstance()在thread2執(zhí)行LazySingleton.getInstance()之前耸别,私有構造器判斷失效健芭。所以如果不是類加載時初始化單例類(比如懶漢式之同步鎖與雙重檢測加鎖),是無法阻止反射攻擊秀姐。

(2)擴展1

不知道有沒有人比較較真慈迈,增加私有靜態(tài)成員變量,增強私有構造器的判斷省有。我們增加一個flag標志(ps:使用更復雜邏輯道理也是相同)痒留。

public class LazySingleton {
    public static LazySingleton lazySingleton = null;
    private static boolean flag = true;

    private LazySingleton() {
        if (flag) {
            flag = false;
        } else {
            throw new RuntimeException("單例構造器禁止反射調(diào)用");
        }
    }

    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }

}

這種方式同樣會被反射攻擊,因為反射可以修改權限設置值蠢沿。

測試代碼

public static void main(String[] args) throws Exception {
    Class objectClass = LazySingleton.class;
    Constructor constructor = objectClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    LazySingleton o1 = LazySingleton.getInstance();
    //修改flag=true
    Field flag = o1.getClass().getDeclaredField("flag");
    flag.setAccessible(true);
    flag.set(o1,true);

    LazySingleton o2 = (LazySingleton) constructor.newInstance();

    System.out.println(o1);
    System.out.println(o2);
    System.out.println(o1 == o2);
}

測試結果伸头,反射攻擊成功,無法阻止反射攻擊舷蟀。

測試結果

(3)擴展2

那為什么不可以通過反射設置靜態(tài)私有變量lazySingleton的值為自己創(chuàng)建的值呢恤磷?哪樣所有的私有構造器方法判斷都會失效,即使類加載時初始化單例類也無法阻止反射攻擊野宜?像這樣扫步。

Field lazySingleton = o1.getClass().getDeclaredField("lazySingleton");
lazySingleton.setAccessible(true);
lazySingleton.set(o1,new LazySingleton());

哈哈哈哈哈哈啊哈哈哈,報錯了吧速缨,忘了我們的構造器是私有了的么锌妻。

'LazySingleton()' has private access in 
'com.desgin.pattern.creational.singleton.LazySingleton'

6. 單例模式的最佳實踐

序列化與反序列化:懶漢式之同步鎖、雙重檢測加鎖旬牲、靜態(tài)內(nèi)部類與餓漢式都必須增加一個readResolve()方法仿粹,不然反序列化回來的不是同一個對象搁吓。并且就算是增加了readResolve()方法反序列化時也會newInstance一個對象,只不過被readResolve()返回的單例對象覆蓋吭历。

反射攻擊:懶漢式之同步鎖堕仔、雙重檢測加鎖由于不是在類加載時初始化單例對象,無法阻止反射攻擊晌区。懶漢式之靜態(tài)內(nèi)部類與餓漢式需要在私有構造器增加判斷摩骨,可以防止反射攻擊。

上面四種單例模式方式需要根據(jù)不同業(yè)務場景使用相對應的單例模式實現(xiàn)朗若。

6.1 最佳實踐

下面介紹一種單例模式的最佳實踐(ps:也是《EffectiveJava》推薦的單例實現(xiàn)方式)

Enum實現(xiàn)單例模式

public enum EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

6.2 序列化攻擊

(1)測試

使用序列化與反序列化測試一下會不會出問題恼五。我們先測試這個枚舉持有的INSTANCE

import java.io.*;

/**
 * Create by lastwhisper on 2019/1/26
 */
public class Test1 {
    public static void main(String[] args) throws Exception {
        EnumInstance instance = EnumInstance.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
        oos.writeObject(instance);

        File file = new File("E:\\singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance)ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

測試結果,序列化與反序列化并不會破壞單例模式

序列化攻擊Enum實現(xiàn)單例模式

再測試枚舉持有的對象data哭懈,看看這個data是不是同一個

import java.io.*;

/**
 * Create by lastwhisper on 2019/1/26
 */
public class Test1 {
    public static void main(String[] args) throws Exception {
        EnumInstance instance = EnumInstance.getInstance();
        instance.setData(new Object());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
        oos.writeObject(instance);

        File file = new File("E:\\singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance)ois.readObject();
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

測試結果灾馒,是同一個data。

序列化攻擊Enum實現(xiàn)單例模式

(2)原理分析

readObject()

在測試類的readObject()方法中遣总,會調(diào)用Object obj = readObject0(false);

Object obj = readObject0(false)

readObject0()方法睬罗,進入switch,case TC_ENUM

case TC_ENUM

readEnum()方法里旭斥,進入一系列校驗容达。在1715行String name = readString(false);,通過readString()方法獲取枚舉對象的名稱name垂券。在1716行Enum en = null;聲明一個Enum類型花盐。在1717行Class cl = desc.forClass();獲取枚舉對象的類型。在1720行en = Enum.valueOf(cl, name);根據(jù)類型和name圆米,對枚舉常量進行初始化卒暂。沒有創(chuàng)建新的對象,維持了單例屬性娄帖。

readEnum()

6.3 反射攻擊

(1)測試以及原理分析

import java.lang.reflect.Constructor;

/**
 * Create by lastwhisper on 2019/1/25
 */
public class Test {
    public static void main(String[] args) throws Exception {
        /*反射測試*/
        Class objectClass = EnumInstance.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();
    }
}

測試結果也祠,拋出異常NoSuchMethodException,獲取構造器時沒有獲得無參構造器近速。

拋出異常NoSuchMethodException

為什么會這樣的呢诈嘿?我們進入java.lang.Enum的源碼中看一下。
在Enum類中只有一個有參構造器

Enum的有參構造器
Enum的有參構造器

修改測試代碼削葱,構造一個有參構造器

Class objectClass = EnumInstance.class;

Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance newInstance = (EnumInstance) constructor.newInstance("gaojun",123456);

測試結果奖亚,異常信息:Cannot reflectively create enum objects

測試結果

我們點進520行錯誤代碼里面,發(fā)現(xiàn)如果是Enum類型Coustructor的newInstance方法就會拋出異常析砸,Cannot reflectively create enum objects昔字。所以無法通過反射創(chuàng)建Enum類型。

無法通過反射創(chuàng)建Enum類型

6.4 Enum實現(xiàn)單例模式的優(yōu)勢

我們使用jad對EnumInstance進行反編譯,查看Enum做單例的優(yōu)勢作郭。

jad EnumInstance.class
jad EnumInstance.class

打開生成的jad文件陨囊。

// 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:   EnumInstance.java
package com.desgin.pattern.creational.singleton;

public final class EnumInstance extends Enum
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/desgin/pattern/creational/singleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)
    {
        super(s, i);
    }

    public Object getData()
    {
        return data;
    }

    public void setData(Object data)
    {
        this.data = data;
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }
    public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];
    
    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

首先EnumInstance類是final類型的無法被繼承,有一個私有構造器夹攒。

private EnumInstance(String s, int i)
    {
        super(s, i);
    }

以及靜態(tài)的final的單例對象蜘醋,在類被加載時就會被靜態(tài)代碼塊(ps:static{})初始化,并且不可被修改咏尝,保證了線程安全压语。加上I/O類、反射類對Enum類型的支持编检,Enum非吧常適合做單例模式胎食。

public static final EnumInstance INSTANCE;
    private Object data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }

Enum單例實現(xiàn)優(yōu)勢總結

  1. 寫法簡單
  2. 線程安全
  3. 懶加載
  4. 避免序列化攻擊
  5. 避免反射攻擊

7. 容器單例

將單例對象都保存在一個容器中

package com.desgin.pattern.creational.singleton;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;

/**
 * Create by lastwhisper on 2019/1/27
 */
public class ContainerSingleton {
    private static Map<String, Object> singletonMap = new HashMap<String, Object>();

    private ContainerSingleton() {
    }
    public static void putInstance(String key, String instance){
        if(StringUtils.isNoneBlank(key) && instance != null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

由于HashMap線程不安全,導致這種容器單例模式也是線程不安全的允懂,這種場景適用于斥季,項目初始化時將需要的單例對象放入Map中。如果改有HashTable累驮,雖然線程安全,但在頻繁get的過程會有同步鎖舵揭,效率低谤专。如果改用CurrentHashMap,此時是靜態(tài)的CurrentHashMap午绳,并且是直接操作的CurrentHashMap置侍,CurrentHashMap并不是絕對的線程安全。

8. 線程單例

這種方式只能保證在一個線程內(nèi)拿到單例對象

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> treadLocalInstance =
            new ThreadLocal<ThreadLocalInstance>() {
                @Override
                protected ThreadLocalInstance initialValue() {
                    return new ThreadLocalInstance();
                }
            };

    private ThreadLocalInstance() {
    }

    public static ThreadLocalInstance getInstance() {
        return treadLocalInstance.get();
    }
}

9. 優(yōu)缺點

優(yōu)點:

  • 在內(nèi)存里只有一個實例拦焚,減少了內(nèi)存開銷
  • 可以避免對資源的多重占用
  • 設置全局訪問點蜡坊,嚴格控制訪問

缺點:

  • 沒有接口,擴展困難

10. 擴展-JDK1.7源碼中的單例模式

10.1 Runtime——單例模式的餓漢式

通過查看java.lang.Runtime靜態(tài)成員變量currentRuntime赎败、getRunTime()方法秕衙、私有構造器,可知是一個單例模式的餓漢式僵刮。

Runtime

10.2 Desktop——容器單例

10.2 Desktop——容器單例
查看 java.awt.Desktop類的getDesktop()方法据忘,是一個同步方法,會從一個AppContext中取值搞糕,如果context中沒有就new一個勇吊,并put進context中。

getDesktop()

查看put方法窍仰,會將值put到this.table中汉规。

put方法

查看this.table是一個HashMap。是一個容器單例驹吮,只不過put方法里面加了同步鎖针史,保證put時是線程安全的晶伦。

table

10.3 ErrorContext——線程單例

org.apache.ibatis.executor.ErrorContext類中使用ThreadLocal保證線程安全,調(diào)用instance()方法創(chuàng)建單例的ErrorContext對象悟民,每個線程自己的錯誤坝辫,線程自己保存。

public class ErrorContext {

  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

  private ErrorContext() {
  }

  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
}

10.4 AbstractFactoryBean——懶漢式

org.springframework.beans.factory.config.AbstractFactoryBeangetObject() 方法中射亏,查看調(diào)用getEarlySingletonInstance()

getObject()

使用了懶漢式近忙,初始化單例對象

getEarlySingletonInstance()

11. 單例模式總結

11.1 單例模式實現(xiàn)方法

單例模式實現(xiàn)方法

11.2 安全性

** 序列化與反序列化:**

  • 懶漢式之同步鎖、雙重檢測加鎖智润、靜態(tài)內(nèi)部類與餓漢式都必須增加一個readResolve()方法及舍,不然反序列化回來的不是同一個對象。并且就算是增加了readResolve()方法反序列化時也會newInstance一個對象窟绷,只不過被readResolve()返回的單例對象覆蓋锯玛。
  • 枚舉實現(xiàn)不會被序列化與反序列化影響

反射攻擊:

  • 懶漢式之同步鎖、雙重檢測加鎖由于不是在類加載時初始化單例對象兼蜈,無法阻止反射攻擊攘残。
  • 懶漢式之靜態(tài)內(nèi)部類與餓漢式需要在私有構造器增加判斷,可以防止反射攻擊为狸。
  • 枚舉類無法反射創(chuàng)建對象歼郭,所有不會被反射影響。

11.3 擴展-CAS實現(xiàn)單例

上面的所有實現(xiàn)的單例方法本質上都使用的是鎖辐棒,不使用鎖的話病曾,有辦法實現(xiàn)線程安全的單例嗎?

有漾根,那就是使用CAS泰涂。

CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時辐怕,只有其中一個線程能更新變量的值逼蒙,而其它線程都失敗,失敗的線程并不會被掛起秘蛇,而是被告知這次競爭中失敗其做,并可以再次嘗試。實現(xiàn)單例的方式如下:

package com.desgin.pattern.creational.singleton;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author lastwhisper
 *
 */
public class CASSingleton {
    private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();

    private CASSingleton() {
    }


    public static CASSingleton getInstance() {
        for (;;) {
            CASSingleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new CASSingleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

這種方式實現(xiàn)的單例有啥優(yōu)缺點嗎赁还?

用CAS的好處在于不需要使用傳統(tǒng)的鎖機制來保證線程安全,CAS是一種基于忙等待的算法,依賴底層硬件的實現(xiàn),相對于鎖它沒有線程切換和阻塞的額外消耗,可以支持較大的并行度妖泄。

CAS的一個重要缺點在于如果忙等待一直執(zhí)行不成功(一直在死循環(huán)中),會對CPU造成較大的執(zhí)行開銷。

另外艘策,如果N個線程同時執(zhí)行到singleton = new Singleton();的時候蹈胡,會有大量對象創(chuàng)建,很可能導致內(nèi)存溢出。所以罚渐,不建議使用這種實現(xiàn)方式却汉。

參考

geely Java設計模式精講 Debug方式+內(nèi)存分析 的單例模式

面試官:不使用synchronized和lock,如何實現(xiàn)一個線程安全的單例荷并?

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末合砂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子源织,更是在濱河造成了極大的恐慌翩伪,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谈息,死亡現(xiàn)場離奇詭異缘屹,居然都是意外死亡,警方通過查閱死者的電腦和手機侠仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門轻姿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逻炊,你說我怎么就攤上這事互亮。” “怎么了余素?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵胳挎,是天一觀的道長。 經(jīng)常有香客問我溺森,道長,這世上最難降的妖魔是什么窑眯? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任屏积,我火速辦了婚禮,結果婚禮上磅甩,老公的妹妹穿的比我還像新娘炊林。我一直安慰自己,他們只是感情好卷要,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布渣聚。 她就那樣靜靜地躺著,像睡著了一般僧叉。 火紅的嫁衣襯著肌膚如雪奕枝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天瓶堕,我揣著相機與錄音隘道,去河邊找鬼。 笑死,一個胖子當著我的面吹牛谭梗,可吹牛的內(nèi)容都是我干的忘晤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼激捏,長吁一口氣:“原來是場噩夢啊……” “哼设塔!你這毒婦竟也來了?” 一聲冷哼從身側響起远舅,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤闰蛔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后表谊,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钞护,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年爆办,在試婚紗的時候發(fā)現(xiàn)自己被綠了难咕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡距辆,死狀恐怖余佃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情跨算,我是刑警寧澤爆土,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站诸蚕,受9級特大地震影響步势,放射性物質發(fā)生泄漏。R本人自食惡果不足惜背犯,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一坏瘩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漠魏,春花似錦倔矾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钧舌。三九已至,卻和暖如春壤巷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瞧毙。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工隙笆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留锌蓄,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓撑柔,卻偏偏與公主長得像瘸爽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子铅忿,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容