單例模式的演進

本部分介紹單例模式,從懶漢式單例講起,介紹懶漢式模式遇到的各類問題:多線程、指令重排序,及其在解決問題的基礎上的各類演進。以後介紹餓漢式單例,餓漢式單例相對簡單,可是失去了延遲加載的優點。還會介紹序列化、反射等對單例模式的破壞與預防;並引伸出相對完美的枚舉單例。還擴展介紹了容器單例,以及ThreadLocal單例。html

一. 概述java

1. 定義:保證一個類僅有一個實例,並提供一個全局訪問點數據庫

2. 類型:建立型設計模式

3. 適用場景:確保任何狀況下都絕對只有一個實例,如:應用配置、線程池、數據庫鏈接池。安全

4. 優缺點:多線程

  4.1 優勢:在內存裏只有一個實例,減小內存開銷;避免對資源的多重佔用ide

  4.2 缺點:沒有接口,擴展困難性能

5. 重點:私有構造器、線程安全、延遲加載、指令重排序、序列化和反序列化安全、反射攻擊測試

6. 實用技能:反編譯、內存原理、多線程Debug字體

7. 相關設計模式:工廠模式(通常把工廠類設計爲單例模式的)、享元模式(經過享元模式和單例模式的結合,完成單例對象的獲取,這種狀況下享元模式相似於單例模式的工廠)

2、懶漢式單例:多線程Debug、指令重排序

1. 懶漢式:懶漢式能夠理解爲這種方式比較懶,有拖延症,其實爲延遲加載,實例的建立要到必須的時候才進行。與之相對應的是餓漢式,這種方式比較積極,在類加載的時候就建立實例。這裏先介紹懶漢式。

2. 線程不安全的懶漢式單例及其逐步優化

咱們對單例模式逐步演化,從最基本的開始:

 1 public class LazySingleton {
 2     private static LazySingleton lazySingleton = null; // 空的實例
 3     private LazySingleton() {};  // 私有構造器
 4     public static LazySingleton getInstance() {  // 公有方法獲取實例
 5         if (lazySingleton == null) {
 6             lazySingleton = new LazySingleton();
 7         }
 8         return lazySingleton;
 9     }
10 }

這種懶漢式單例模式,在多線程的狀況下是不安全的。考慮這麼一種狀況,有兩個線程0和1,當線程0運行到第6行建立實例但還沒賦值時切換到線程1,線程1運行到第5行判斷lazySingleton爲空,繼續運行,這樣就建立了兩個實例。雖然最後賦值給lazySingleton的是一個單例,但在多個線程的狀況下卻會建立多個單例,若是單例佔用內存較多,則頗有可能形成系統故障。下面用多線程Debug的方式模擬兩個線程的狀況。

多線程安全問題建立線程類和測試類,代碼以下:

 1 // 線程類
 2 public class T implements Runnable{
 3 
 4     @Override
 5     public void run() {
 6         LazySingleton lazySingleton = LazySingleton.getInstance();
 7         System.out.println(Thread.currentThread().getName() + " " + lazySingleton);
 8     }
 9 
10 }
// 測試類
public class Test {
    public static void main(String[] args) {
        Thread t0 = new Thread(new T());
        Thread t1 = new Thread(new T());
        t0.start();
        t1.start();
        
        System.out.println("The end...");    
    }
}

上述代碼直接運行的話,輸出的結果是同樣的,可是當咱們Debug就會發現建立了多個實例對象。

多線程debug:

1)首先在線程類的run()第6行打個斷點(注意:斷點的位置必定要正確,run方法或者run之後調用的方法裏,不然的話,程序跑完了,debug模式裏也只有一個主線程在跑),而後點擊debug,程序運行會停在斷點位置,觀察debug一欄,會看到多個線程。以下圖

上圖展示了斷點的位置和多個線程。

2)切換某個線程時,適用鼠標點擊就能夠。在此首先執行線程1,進入getInstance方法,執行到單例類的第6行,此時還未給lazySingleton賦值,該變量仍然爲null。以下圖所示:

3)這時,鼠標點擊線程0,切換到0線程,一樣執行到單例類第6行,因爲lazySingleton爲null,因此能夠經過if。以下圖所示:

4)切換回線程1,執行到單例類第8行,這時能夠看到lazySingleton已經賦值,值爲:26,以下圖所示:

5)再切換回線程0,執行一樣的執行到單例類第8行,這時能夠看到,lazySingleton的值已經改變爲:31,以下圖所示。

6)執行程序到最後輸出結果以下圖所示:

上述過程使用了多線程debug的技能,輸出結果同樣,這是由於後一個線程從新賦值了,而且是在從新賦值後進行的return,可是建立了兩個單例對象。若在第5步不切換回線程0,而是直接讓線程1運行結束,再切換回線程0,讓其運行結束,那麼輸出的將是兩個不一樣的結果。

對於上述線程不安全的懶漢式單例模式,採用加sychronized關鍵字的方式改進:

1     public synchronized static LazySingleton getInstance2() {  // synchronized鎖靜態類
2         if (lazySingleton == null) {
3             lazySingleton = new LazySingleton();
4         }
5         return lazySingleton;
6     }

synchroized鎖靜態類使方法變成同步方法,注意synchroized加在靜態方法上鎖的是類的class文件,synchroized加在非靜態方法上鎖的是堆中的對象。上述代碼還有另外一種寫法:

1     public static LazySingleton getInstance3() {  
2         synchronized (LazySingleton.class) { // 鎖類
3             if (lazySingleton == null) {
4                 lazySingleton = new LazySingleton();
5             }
6         }
7         return lazySingleton;
8     }

上述兩種代碼效果同樣,都是鎖class。由於鎖的範圍過大, 因此會影響性能。下面有更優化的方式:兼顧性能、線程安全,同時是懶加載的。

雙重檢查式懶漢

 1 public class LazyDoubleCheckSingleton {
 2     private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 空的實例
 3     private LazyDoubleCheckSingleton() {};  // 私有構造器
 4     
 5     public static LazyDoubleCheckSingleton getInstance() {  // 公有方法獲取實例
 6         if (lazyDoubleCheckSingleton == null) {   // 檢查1
 7             synchronized (LazyDoubleCheckSingleton.class) { // 鎖類
 8                 if (lazyDoubleCheckSingleton == null) {  // 檢查2
 9                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
10                 }
11             }
12         }
13         return lazyDoubleCheckSingleton;
14     }
15 }

上述代碼看似安全高效,在第一次建立單例對象後,不須要再次進入sychronized。但想象不到的安全隱患卻潛藏於第6行和第9行,這涉及到另外一個知識點:指令重排序。在第9行看似一個步驟,其實涉及到對象的建立過程,主要有三個步驟:1. 分配內存,2. 初始化對象,3. 指針指向內存地址。這三個步驟中2 3的順序是能夠改變的,即順序能夠爲123或132。當初始化順序爲132時,若執行到3,切換另外一個線程,該線程執行到第6行,lazyDoubleCheckSingleton不爲空,而後第13行返回,由於此時爲執行初始化步驟2,則返回的實例對象未初始化,因此會影響接下來的進程。

爲了消除指令重排序形成的影響,能夠採起禁止指令重排序指令重排序對其餘線程不可見的方式。

1)禁止指令重排序可使用volatile關鍵字,詳情點擊連接。上述代碼也就改爲了

 1 public class LazyDoubleCheckSingleton {
 2     private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; // 加了 volatile
 3     private LazyDoubleCheckSingleton() {};  // 私有構造器
 4     
 5     public static LazyDoubleCheckSingleton getInstance() {  // 公有方法獲取實例
 6         if (lazyDoubleCheckSingleton == null) {   // 檢查1
 7             synchronized (LazyDoubleCheckSingleton.class) { // 鎖類
 8                 if (lazyDoubleCheckSingleton == null) {  // 檢查2
 9                     lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
10                 }
11             }
12         }
13         return lazyDoubleCheckSingleton;
14     }
15 }

 2)防止其餘線程看到指令重排序的方式能夠採用靜態內部類的方式,代碼以下:

 1 public class StaticInnerClassSingleton {
 2     private StaticInnerClassSingleton() {}; // 私有構造方法
 3     
 4     private static class InnerClass{
 5         private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
 6     }
 7     
 8     public static StaticInnerClassSingleton getInstance() {
 9         return InnerClass.staticInnerClassSingleton;
10     }
11 }

JVM在類的初始化階段,執行類的初始化時,JVM會獲取一個鎖,這個鎖會同步多個線程對一個類的初始化。上述特性能夠實現基於靜態內部類的、線程安全的、延遲初始化方案。這樣當一個線程執行類的初始化時,其餘線程會被鎖在外面。

觸發類初始化的狀況有如下5種:1. 有一個A類型的實例被建立;2. A類中聲明的靜態方法被調用;3. A類中的靜態成員被賦值;4. A類中的靜態成員被使用,且該成員不是常量成員;5. A類是頂級類,且該類中有嵌套的斷言語句。

假設線程0獲取到StaticInnerClassSingleton 對象的初始化鎖,這時線程0執行該靜態內部類的初始化。這時即便初始化步驟2. 初始化對象,3. 指針指向內存地址,之間存在重排序,可是線程1也是沒法看到的。因此這裏的關鍵就在於InnerClass的初始化鎖被哪一個線程拿到,哪一個線程就執行初始化。

總結:對於初始的懶漢式單例,因爲存在多線程不安全的狀況,因此須要加sycnrhnized關鍵字;但該關鍵字會下降效率,因此出現了雙重檢查機制;對於雙重檢查機制,存在指令重排序的問題,爲防止指令重排序使用了volatile關鍵字、或使指令重排序對其餘線程不可見使用了靜態內部類。

在上述敘述中,問題用紅色字體標出,解決方案用綠色字體標出。

 2、餓漢式單例:

1. 餓漢式:在類加載的時候,就完成實例化。

1 public class HungrySingleton {
2     private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); // 類加載時就初始化
3     private HungrySingleton() {} // 私有構造方法
4     
5     public static HungrySingleton getInstance() {
6         return HUNGRY_SINGLETON;
7     }
8 }

上述爲餓漢式的基本模式,優勢爲:寫法簡單、類加載時就完成初始化避免了線程同步問題。缺點是沒有延遲加載的效果,單例類通常比較大,若是這個類從始至終沒有被用過,會形成內存的浪費。整體來講,餓漢式是最簡單的,若是資源浪費少的話,這種模式很是方便。上述代碼還有另一種實現方式:

 1 public class HungrySingleton {
 2     private final static HungrySingleton HUNGRY_SINGLETON; 
 3     static {
 4         HUNGRY_SINGLETON = new HungrySingleton(); // 類加載時就初始化
 5     }
 6     
 7     private HungrySingleton() {} // 私有構造方法
 8     
 9     public static HungrySingleton getInstance() {
10         return HUNGRY_SINGLETON;
11     }
12 }

3、序列化破壞單例解決方案與 原理分析

    能夠思考這樣一個問題:當把單例對象序列化到一個文件中,而後再把它反序列化出來,這樣生成的對象和原來的對象仍是同一個嗎?

    下面使用餓漢式測試,測試前餓漢單例類先實現Serializable接口,而後編寫測試類以下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         // 序列化寫
 4         HungrySingleton instance = HungrySingleton.getInstance();
 5         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
 6         oos.writeObject(instance);   
 7         
 8         // 序列化讀
 9         File file = new File("singleton_file");
10         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 
11         HungrySingleton newInstance = (HungrySingleton)ois.readObject(); 
12         
13         // 比較
14         System.out.println(instance);
15         System.out.println(newInstance);
16         System.out.println(instance == newInstance);
17     }
18 }

運行測試類輸出發現,instance和newInstance並不同,序列化破壞了單例模式生成了不一樣的對象。爲了解決上述問題並理解其原理,須要探究ObjectInputStream.readObject()的源碼。在readObject()方法中調用了readObject0(),在readObject0()方法中的switch語句中調用了readOrdinaryObject方法()。下面是該方法的源碼,關鍵處已註釋。

 1 private Object readOrdinaryObject(boolean unshared)
 2         throws IOException
 3     {
 4         if (bin.readByte() != TC_OBJECT) {
 5             throw new InternalError();
 6         }
 7 
 8         ObjectStreamClass desc = readClassDesc(false);
 9         desc.checkDeserialize();
10 
11         Class<?> cl = desc.forClass();
12         if (cl == String.class || cl == Class.class
13                 || cl == ObjectStreamClass.class) {
14             throw new InvalidClassException("invalid class descriptor");
15         }
16 
17         Object obj;
18         try {
19             obj = desc.isInstantiable() ? desc.newInstance() : null; // 反射建立對象
20         } catch (Exception ex) {
21             throw (IOException) new InvalidClassException(
22                 desc.forClass().getName(),
23                 "unable to create instance").initCause(ex);
24         }
25 
26         passHandle = handles.assign(unshared ? unsharedMarker : obj);
27         ClassNotFoundException resolveEx = desc.getResolveException();
28         if (resolveEx != null) {
29             handles.markException(passHandle, resolveEx);
30         }
31 
32         if (desc.isExternalizable()) {
33             readExternalData((Externalizable) obj, desc);
34         } else {
35             readSerialData(obj, desc);
36         }
37 
38         handles.finish(passHandle);
39 
40         if (obj != null &&
41             handles.lookupException(passHandle) == null &&
42             desc.hasReadResolveMethod())  // 判斷是否有readResolve方法
43         {
44             Object rep = desc.invokeReadResolve(obj); // 反射調用readResolve方法
45             if (unshared && rep.getClass().isArray()) {
46                 rep = cloneArray(rep);
47             }
48             if (rep != obj) {
49                 // Filter the replacement object
50                 if (rep != null) {
51                     if (rep.getClass().isArray()) {
52                         filterCheck(rep.getClass(), Array.getLength(rep));
53                     } else {
54                         filterCheck(rep.getClass(), -1);
55                     }
56                 }
57                 handles.setObject(passHandle, obj = rep);
58             }
59         }
60 
61         return obj;
62     }

在第19行,經過反射建立單例對象,此時反射建立的單例對象與getInstance()得到的對象不一樣,因此測試類中輸出false。爲使其相同,咱們繼續往下看。最後返回的是obj,在第57行有將rep賦值給obj的操做。爲知足其條件,首先看到第42行,點進去看源碼判斷是否有readResolve()方法,在第44行反射調用readResolve()方法將其結果賦值給rep。所以咱們在此處這樣改寫單例類:

 1 public class HungrySingleton implements Serializable{
 2     private final static HungrySingleton HUNGRY_SINGLETON; 
 3     static {
 4         HUNGRY_SINGLETON = new HungrySingleton(); // 類加載時就初始化
 5     }
 6     
 7     private HungrySingleton() {} // 私有構造方法
 8     
 9     public static HungrySingleton getInstance() {
10         return HUNGRY_SINGLETON;
11     }
12     
13     // readResolve方法
14     private Object readResolve() {
15         return HUNGRY_SINGLETON;
16     }
17 }

再次運行測試類,兩個對象比較,返回ture。爲更詳細的瞭解,能夠debug看其具體執行過程。同時注意,在上述序列化和反序列化的過程當中,已經實例化對象了,只是沒有返回。

 4、反射攻擊解決方案及 原理分析

一樣用簡單的餓漢模式進行演示,反射攻擊的測試類以下:

 1 public class Test {
 2     
 3     public static void main(String[] args) throws Exception {
 4         Class objCla = HungrySingleton.class;
 5         Constructor constructor = objCla.getDeclaredConstructor();
 6         constructor.setAccessible(true); // 把權限置爲ture,放開權限
 7         
 8         HungrySingleton instance = HungrySingleton.getInstance();
 9         HungrySingleton newInstance = (HungrySingleton)constructor.newInstance();
10         
11         System.out.println(instance);
12         System.out.println(newInstance);
13         System.out.println(instance == newInstance);
14     }
15 }

上述代碼輸出結果爲false,對於餓漢式單例,因爲它在類加載時就已經生成了對象,所以咱們能夠經過改動構造方法來防止在類加載後再次建立對象。具體代碼以下:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton HUNGRY_SINGLETON; 
    static {
        HUNGRY_SINGLETON = new HungrySingleton(); // 類加載時就初始化
    }
    
    private HungrySingleton() { // 私有構造方法
        if (HUNGRY_SINGLETON != null) { // 拋出異常禁止反射調用
            throw new RuntimeException("單例構造器禁止反射調用");
        }
    } 
    
    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }
}    

運行後發現,反射調用構造方法時會拋出異常。可是上述方式僅適用於靜態內部類和餓漢式的方式。

對於不是在類加載時就建立單例的狀況,不可使用上述方式。

5、枚舉單例、原理源碼及反編譯

 對於枚舉單例,將主要關注它在序列化和反射攻擊中的表現。枚舉單例代碼以下:

 1 public enum EnumInstance {
 2     INSTANCE;
 3     private Object data; // 測試的主要爲枚舉類持有的對象data
 4 
 5     public Object getData() {
 6         return data;
 7     }
 8 
 9     public void setData(Object data) {
10         this.data = data;
11     }
12     
13     public static EnumInstance getInstance() {
14         return INSTANCE;
15     }
16 }

1)序列化測試類的代碼以下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         EnumInstance instance = EnumInstance.getInstance();
 4         instance.setData(new Object());
 5         
 6         // 枚舉單例類測試序列化
 7         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
 8         oos.writeObject(instance);  
 9         File file = new File("singleton_file");
10         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); 
11         EnumInstance newInstance = (EnumInstance)ois.readObject(); 
12         
13         System.out.println(instance.getData());
14         System.out.println(newInstance.getData());
15         System.out.println(instance.getData() == newInstance.getData());
16     }
17 }

運行測試類後,兩個instance輸出結果同樣。接下來經過源碼瞭解枚舉不受序列化影響的緣由:打開ObjectInputStream.readObject()的源碼。在readObject()方法中調用了readObject0(),在readObject0()方法中的switch語句中調用了readEnum方法()。下面是該方法的源碼,關鍵處已註釋

    private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false); // 獲取枚舉對象的名稱
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name); // 獲取枚舉常量,由於枚舉中name是惟一的,而且對應一個枚舉常量,所以這裏得到的是惟一的常量對象,沒有建立新的對象
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

2)反射的測試類代碼以下:

 1 public class Test {
 2     public static void main(String[] args) throws Exception {
 3         Class objCla = EnumInstance.class;
 4         Constructor constructor = objCla.getDeclaredConstructor(String.class, int.class); // 枚舉類的構造方法帶有兩個參數
 5         constructor.setAccessible(true); // 把權限置爲ture,放開權限
 6         
 7         EnumInstance instance = EnumInstance.getInstance();
 8         EnumInstance newInstance = (EnumInstance)constructor.newInstance("haha", 666);
 9         
10         System.out.println(instance);
11         System.out.println(newInstance);
12         System.out.println(instance == newInstance);
13     }
14 }

上述代碼爲枚舉反射的測試代碼,運行上述代碼能夠發現運行不到10行,由於在第8行會報錯:「java.lang.IllegalArgumentException: Cannot reflectively create enum objects」。具體的能夠在運行時打開Constructor類查看報錯處的源碼。

3)枚舉類自己的優點

瞭解枚舉類自己,要把它反編譯。這裏反編譯枚舉類使用的時JAD,能夠到這裏下載。下載完成後,解壓,配置環境變量便可使用,具體可百度。這裏對EnumInstance類進行反編譯,命令爲:

jad EnumInstance.class

反編譯結果的後綴名爲.jad,此處使用Notepad++打開反編譯後的結果以下,關鍵處已註釋:

 1 public final class EnumInstance extends Enum // final --> 類不能被繼承
 2 {
 3 
 4     private EnumInstance(String s, int i)  // 私有構造器
 5     {
 6         super(s, i);
 7     }
 8 
 9     public Object getData()
10     {
11         return data;
12     }
13 
14     public void setData(Object data)
15     {
16         this.data = data;
17     }
18 
19     public static EnumInstance getInstance()
20     {
21         return INSTANCE;
22     }
23 
24     public static EnumInstance[] values()
25     {
26         EnumInstance aenuminstance[];
27         int i;
28         EnumInstance aenuminstance1[];
29         System.arraycopy(aenuminstance = ENUM$VALUES, 0, aenuminstance1 = new EnumInstance[i = aenuminstance.length], 0, i);
30         return aenuminstance1;
31     }
32 
33     public static EnumInstance valueOf(String s)
34     {
35         return (EnumInstance)Enum.valueOf(pattern/creational/singletion/EnumInstance, s);
36     }
37 
38     public static final EnumInstance INSTANCE;   // 靜態的類變量
39     private Object data;
40     private static final EnumInstance ENUM$VALUES[];
41 
42     static    // 靜態塊加載
43     {
44         INSTANCE = new EnumInstance("INSTANCE", 0);
45         ENUM$VALUES = (new EnumInstance[] {
46             INSTANCE
47         });
48     }
49 }

從反編譯結果能看出,枚舉類的構造器是私有的,而且類變量是static final,且在靜態塊中加載。而且有序列化和反射方面的優點,因此枚舉類在建立單例對象上具有原生優點。

6、基於容器的單例模式

 基於容器的單例模式,相似於享元模式。代碼以下:

 1 public class ContainerSingleton {
 2     private ContainerSingleton() {}
 3     
 4     private static Map<String, Object> singletonMap = new HashMap<String, Object>(); // 用map存儲單例
 5                                                                                      // hashmap不是線程安全的
 6     public static void putInstance(String key, Object instance) {
 7         if (key != null && key.length() != 0 && instance != null) {
 8             if (!singletonMap.containsKey(key)) {
 9                 singletonMap.put(key, instance);
10             }
11         }
12     }
13     
14     public static Object getInstance(String key) {
15         return singletonMap.get(key);
16     }
17 }

此處使用map存儲單例,HashMap不是線程安全的,可是在類加載時直接加載HashMap這樣用也能夠,但要考慮具體狀況。考慮下數狀況,有兩個線程,線程0先put進kv,而後get數據;線程2再put進kv,而後get數據。這事兩個線程使用一樣的key不一樣的value那麼得到的結果是不同的;若線程0先put,而後線程1put,而後再get,那麼得到的結果是同樣的。

若將HashMap改爲HashTable會變成線程安全的,可是會影響性能;如果改爲ConcurrentHashMap,在此場景中,使用了靜態的ConcurrentHashMap而且直接操做了map,ConcurrentHashMap並非絕對的線程安全。綜上,不考慮反射、序列化等狀況,容器單例模式也是有必定適用場景。

容器能夠統一管理單例對象,節省資源,但線程並不安全。

 7、ThreadLocal線程單例(可保證線程惟一,不能保證全局惟一)

使用ThreadLocal類建立在線程內惟一的單例,代碼以下:

 1 public class ThreadLocalInstance {
 2     private static final ThreadLocal<ThreadLocalInstance> THREAD_LOCAL 
 3         = new ThreadLocal<ThreadLocalInstance>() { // 匿名內部類
 4         protected ThreadLocalInstance initialValue() {  // 重寫方法
 5             return new ThreadLocalInstance();
 6         }
 7     };
 8     
 9     private ThreadLocalInstance() {}
10     
11     public static ThreadLocalInstance getInstance() {
12         return THREAD_LOCAL.get();
13     } 
14 }

修改T類以下:

1 public class T implements Runnable{
2 
3     @Override
4     public void run() {
5         ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
6         System.out.println(Thread.currentThread().getName() + " " + instance);
7     }
8 
9 }

測試:

 1 public class Test {
 2     public static void main(String[] args) {
 3         ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
 4         System.out.println(instance);
 5         System.out.println(instance);
 6         System.out.println(instance);
 7         System.out.println(instance);
 8         System.out.println(instance);
 9         
10         Thread t0 = new Thread(new T());
11         Thread t1 = new Thread(new T());
12         t0.start();
13         t1.start();
14         
15         System.out.println("The end...");    
16     }
17 }

運行上述代碼能夠發現main線程中的輸出結果是同樣的,main,t0,t1的輸出結果各不相同。ThreadLocal隔離了多個線程對資源的訪問衝突,對於多線程資源共享的問題,使用同步鎖是時間換空間的,使用ThreadLocal是空間換時間。

8、源碼中的應用

1)java.lang.Runtime類,屬於餓漢式單例。

2)java.awt.Desktop類的getDesktop屬於容器單例,可是加了各類sychronized進行同步控制。

相關文章
相關標籤/搜索