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