java設計模式精講 Debug 方式+記憶體分析 第8章 單例模式
單例模式
- 8-1 單例模式講解
- 8-2 單例設計模式-懶漢式及多執行緒Debug實戰
- 8-3 單例設計模式-DoubleCheck雙重檢查實戰及原理解析
- 8-4 單例設計模式-靜態內部類-基於類初始化的延遲載入解決方案及原理解析
- 8-5 單例設計模式-餓漢式
- 8-6 單例設計模式-序列化破壞單例模式原理解析及解決方案
- 8-7 單例設計模式-反射攻擊解決方案及原理分析
- 8-8 單例設計模式-Enum列舉單例、原理原始碼解析以及反編譯實戰
- 8-9 單例設計模式-容器單例
- 8-10 單例設計模式-ThreadLocal執行緒單例
- 8-11 單例模式原始碼分析(jdk+spring+mybatis)
8-1 單例模式講解
8-2 單例設計模式-懶漢式及多執行緒Debug實戰
懶漢式單例:
public class LazySingleton {
/** 懶漢模式的話,開始沒有進行初始化 */
private static LazySingleton lazySingleton = null;
/** 構造器要進行私有化 */
private LazySingleton (){
}
public static LazySingleton getInstance() {
/** 這個是有執行緒安全的問題 */
if (lazySingleton == null) {
/**
* 如果一個執行緒進來了,但是這個時候,在new例項的時候,阻塞了或者還沒有new出例項,
* 這個時候,另外一個執行緒判斷lazySingleton依然是空的,那麼就這時候,也進來了,
* 那麼這個時候,就是有執行緒安全問題的
*/
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
我們來測試一下:
public class Test {
public static void main(String[]args){
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton);
}
}
debug除錯:
我們再來看看多執行緒的時候,會出現什麼問題:
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("program end");
}
}
program end
Thread-1 [email protected]46
Thread-0 [email protected]46
這個時候,我們就要用到多執行緒debug來進行除錯:
模擬兩個執行緒,一個執行緒在進入if之後,還沒有new出例項, 這個時候,另外一個執行緒也進來了,if判斷這個時候還沒有例項,於是也進入了if裡面,這個時候,就new出來兩個例項:
然後,我們在切換到thread0,讓它賦值上:
接著我們切換到Thread1:
我們接著向下執行:
Thread0和Thread1都放過:
雖然此時,兩個物件還是同一個物件,但是是經過了修改了的:
我們再debug返回不同的物件:
我們先都兩個執行緒進入if判斷,然後一個執行緒直接執行完成,再另外一個執行緒執行完成,這個時候,返回的就是兩個物件了:
我們對懶漢式單例模式的執行緒安全問題有幾種解決方案:
方式一:在獲取例項的方法上新增synchronized關鍵字
如果鎖載入靜態方法上 ,那麼就相當於鎖是加在這個類的class檔案;如果不是靜態方法相當於是在堆記憶體中生成的物件:
public class LazySingleton {
/** 懶漢模式的話,開始沒有進行初始化 */
private static LazySingleton lazySingleton = null;
/** 構造器要進行私有化 */
private LazySingleton(){
}
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
方式二:把獲取例項的方法內新增synchronized程式碼塊
public class LazySingleton {
/** 懶漢模式的話,開始沒有進行初始化 */
private static LazySingleton lazySingleton = null;
/** 構造器要進行私有化 */
private LazySingleton(){
}
public static LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
}
我們再執行Thread1,發現執行不下去了,已經是阻塞的狀態了:
最後拿的就是同一個物件:
但是,我們知道,加鎖和解鎖的時候,是會帶來額外的開銷,對效能會有一定的影響;我們再來進行演進,在效能和安全上進行平衡;
8-3 單例設計模式-DoubleCheck雙重檢查實戰及原理解析
我們 可以這樣來寫:
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;
}
}
這個時候,會有一個風險:那就是發生了重排序:
public class LazyDoubleCheckSingleton {
/** 懶漢模式的話,開始沒有進行初始化 */
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
/** 構造器要進行私有化 */
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance() {
/** 如果2和3進行重排序,那麼這裡的判斷並不為空,這個時候,實際上物件還沒有初始化好,就可以進行這個判斷 */
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
/**
* 實際上有三個步驟:
* 1. 分配記憶體給這個物件
* 2. 初始化物件
* 3.設定lazyDoubleCheckSingleton指向剛分配的記憶體地址
* 2和3的順序有可能會被顛倒,
*
* 這個時候,就規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定
* 它保證了重排序不會改變單執行緒內的程式執行結果
*/
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
下圖就是表示單執行緒的情況:規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定,它保證了重排序不會改變單執行緒內的程式執行結果。
現在來看一下多執行緒的情況:
我們可以嘗試不允許重排序:
我們在初始化的時候,給它加上一個volatile關鍵字:這個時候,就可以實現執行緒安全的延遲初始化,這樣的話,重排序就是會被禁止,在多執行緒的時候,CPU也有共享記憶體,我們加上了這個關鍵字了之後,所有執行緒就能看到共享記憶體的最新狀態,保證了記憶體的可見性,使用volatile的時候,在進行寫操作的時候,會多出一些彙編程式碼,起到兩個作用1)將當前處理器快取好的資料寫回到系統記憶體中,其他記憶體從共享記憶體中同步資料,這樣的話,就保證了共享記憶體的可見性,這裡就是使用了快取一致性的協議,當發現快取記憶體中的資料無效,會重新從系統記憶體中把資料讀回處理器的記憶體裡;
public class LazyDoubleCheckSingleton {
/** 懶漢模式的話,開始沒有進行初始化 */
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
/** 構造器要進行私有化 */
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance() {
/** 如果2和3進行重排序,那麼這裡的判斷並不為空,這個時候,實際上物件還沒有初始化好,就可以進行這個判斷 */
if (lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazyDoubleCheckSingleton == null) {
/**
* 實際上有三個步驟:
* 1. 分配記憶體給這個物件
* 2. 初始化物件
* 3.設定lazyDoubleCheckSingleton指向剛分配的記憶體地址
* 2和3的順序有可能會被顛倒,
*
* 這個時候,就規定所有的執行緒在執行java程式的時候,必須要遵守intra-thread semantics這麼一個規定
* 它保證了重排序不會改變單執行緒內的程式執行結果
*/
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
通過volatile和doubleCheck的這種方式既兼顧了效能,又兼顧了執行緒安全的問題;
我們來進行測試一下:
這個時候,拿到這個就是同一個物件:
8-4 單例設計模式-靜態內部類-基於類初始化的延遲載入解決方案及原理解析
這個就是用靜態內部類來實現單例模式:
public class StaticInnerClassSingleton {
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
8-5 單例設計模式-餓漢式
在類載入的時候,就完成了例項化,避免了執行緒同步的問題,缺點就是在類載入的時候,就完成了初始化,沒有延遲載入,這個時候,就是會造成記憶體的浪費
public class HungrySingleton {
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
也可以這樣來寫:
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
餓漢式和懶漢式最大的區別就是在有沒有延遲載入;
8-6 單例設計模式-序列化破壞單例模式原理解析及解決方案
我們給這類實現一個序列化介面:
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
我們來測試:
public class Test {
public static void main(String[]args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("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);
}
}
輸出結果:
[email protected]1dae
[email protected]7951
false
這個時候,通過序列化和反序列化拿了不同的物件;而我們希望拿到的是同一的物件,我們可以這樣來做:
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
/** 我們加上這樣的一個方法 */
private Object readResolve() {
return hungrySingleton;
}
}
我們再來進行測試,這個時候,兩個物件就是同一個物件:
[email protected]1dae
[email protected]1dae
true
我們通過用debug的方式檢視原始碼,我們可以看出這個方法是通過反射出來的:
一旦,我們在程式中使用了序列化的時候,一定要考慮序列化對單例破壞;
8-7 單例設計模式-反射攻擊解決方案及原理分析
我們用反射來寫:
public class Test {
public static void main(String[]args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
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);
}
}
測試結果:
[email protected]cd21
[email protected]5dca
false
現在,我們就來寫反射防禦的程式碼:
在用靜態內部類來生成的也可以用上這個反射防禦的方式:
public class StaticInnerClassSingleton {
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if (InnerClass.staticInnerClassSingleton != null) {
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
}
8-8 單例設計模式-Enum列舉單例、原理原始碼解析以及反編譯實戰
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;
}
}
我們要呼叫方法的話,那麼我們就是可以這樣來寫:
public enum EnumInstance {
INSTANCE {
@Override
protected void printTest() {
System.out.println("Geely Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
8-9 單例設計模式-容器單例
我們可以這樣來寫:
public class ContainerSingleton {
private static Map<String, Object> singletonMap = new HashMap<>();
public static void putInstance(String key,Object instance) {
if (StringUtils.isNotBlank(key) && instance!=null) {
if