【項目實戰】多線程環境下正確創建單例


前言

對項目代碼進行掃描時,出現靜態掃描嚴重問題,發現是由於多線程環境下沒有正確創建單例所導致。

問題分析

本項目使用的JDK 1.7+

項目代碼如下(修改了類名,但核心沒變)

static class Singleton {
	private static volatile Singleton cache = null;
	private static Object mutexObj = new Object();
		
	private Singleton() {
		
	}
		
	public static Singleton getInstance() {
		Singleton tmp = cache;
	    if (tmp == null) {
			synchronized (mutexObj) {
				if (tmp == null) {	                	
					tmp = new Singleton();
					cache = tmp;  
				}	              
			}
		}
		return tmp;
	}
}

按照項目生成單例代碼,使用如下測試類進行測試


public class Test {
	public static void main(String[] args) {	
		for (int i = 0; i < 3; i++) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
				}
				
			});
			thread.setName("Thread" + i);
			thread.start();
		}
		
	}

	static class Singleton {
		private static volatile Singleton cache = null;
		private static Object mutexObj = new Object();
		
		private Singleton() {
		
		}
		
		public static Singleton getInstance() {
			Singleton tmp = cache;
	    	if (tmp == null) {
				synchronized (mutexObj) {
					if (tmp == null) {	                	
						tmp = new Singleton();
						cache = tmp;  
					}	              
				}
			}
			return tmp;
		}
	}
}


輸出結果如下:

Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4
Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4
Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@304e94a4

從結果看,都生成了同一個實例,似乎不存在問題,多線程環境下確實不太好重現問題,現改動代碼如下:


static class Singleton {
	private static volatile Singleton cache = null;
	private static Object mutexObj = new Object();
		
	private Singleton() {
		
	}
		
	public static Singleton getInstance() {
		Singleton tmp = cache;
	    if (tmp == null) {
	        System.out.println(Thread.currentThread().getName() + " in outer if block");
	        synchronized (mutexObj) {
				System.out.println(Thread.currentThread().getName() + " in synchronized block");
	            if (tmp == null) {	 
	               	System.out.println(Thread.currentThread().getName() + " in inner if block");
	               	tmp = new Singleton();
	                cache = tmp;  
	            }	
	            System.out.println(Thread.currentThread().getName() + " out inner if block");
			}
	        System.out.println(Thread.currentThread().getName() + " out synchronized block");
	    }
	    System.out.println(Thread.currentThread().getName() + " out outer if block");
	    return cache;
	}
}


上述代碼中添加了Thread.sleep(1)這條語句,其中,Thread.sleep(1)進行休眠時,線程不會釋放擁有的鎖,並且打印了相關的語句,便於查看線程正運行在哪里的狀態。

再次測試,輸出結果如下:

Thread2 in outer if block
Thread1 in outer if block
Thread0 in outer if block
Thread2 in synchronized block
Thread2 in inner if block
Thread2 out inner if block
Thread2 out synchronized block
Thread0 in synchronized block
Thread2 out outer if block
Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@60b07af1
Thread0 in inner if block
Thread0 out inner if block
Thread0 out synchronized block
Thread1 in synchronized block
Thread0 out outer if block
Thread1 in inner if block
Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@625795ce
Thread1 out inner if block
Thread1 out synchronized block
Thread1 out outer if block
Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@642c39d2

從結果看,生成了3個不同的實例,並且每個線程都執行了完整的流程,並且可知單例的創建存在問題。在分析原因前簡單了解下多線程模型,多線程模型如下:

多線程內存模型

每個線程有自己獨立的工作空間,線程間進行通信是通過主內存完成的,想了解詳細內容可參見如下鏈接:內存模型深入理解java內存模型

知道每個線程會有一份tmp拷貝后,配合打印輸出,就不難分析出原因。

問題解決

按照《Effective Java》一書中創建單例的推薦,可使用如下兩種解決方法

雙重鎖檢查機制

需要配合volatile關鍵字使用,並且需要JDK版本在1.5以上,核心代碼如下


static class Singleton {
	private static volatile Singleton cache = null;
	private static Object mutexObj = new Object();
		
	private Singleton() {
		
	}
		
	public static Singleton getInstance() {
		Singleton tmp = cache;
	    if (tmp == null) {
			tmp = cache;
			synchronized (mutexObj) {
				if (tmp == null) {	                	
					tmp = new Singleton();
					cache = tmp;  
				}	              
			}
		}
		return tmp;
	}
}

進行如下測試(添加打印語句方便分析):


public class Test {
	public static void main(String[] args) {	
		for (int i = 0; i < 3; i++) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
				}
				
			});
			thread.setName("Thread" + i);
			thread.start();
		}
		
	}
	
	static class Singleton {
		private static volatile Singleton cache = null;
		private static Object mutexObj = new Object();
		
		private Singleton() {
		
		}
		
		public static Singleton getInstance() {
			Singleton tmp = cache;
	        if (tmp == null) {
	        	System.out.println(Thread.currentThread().getName() + " in outer if block");
	            synchronized (mutexObj) {
	            	System.out.println(Thread.currentThread().getName() + " in synchronized block");
	            	tmp = cache;
	                if (tmp == null) {	 
	                	System.out.println(Thread.currentThread().getName() + " in inner if block");
	                	tmp = new Singleton();
	                	try {
	                		Thread.sleep(1);
	                	} catch (InterruptedException e) {
	                		e.printStackTrace();
	                	}
	                    cache = tmp;  
	                }	
	                System.out.println(Thread.currentThread().getName() + " out inner if block");
	            }
	            System.out.println(Thread.currentThread().getName() + " out synchronized block");
	        }
	        System.out.println(Thread.currentThread().getName() + " out outer if block");
	        return tmp;
		}
	}
}


輸出結果如下:

Thread0 in outer if block
Thread0 in synchronized block
Thread0 in inner if block
Thread2 in outer if block
Thread1 in outer if block
Thread0 out inner if block
Thread0 out synchronized block
Thread1 in synchronized block
Thread1 out inner if block
Thread1 out synchronized block
Thread1 out outer if block
Thread0 out outer if block
Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f
Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f
Thread2 in synchronized block
Thread2 out inner if block
Thread2 out synchronized block
Thread2 out outer if block
Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@13883d5f

從結果中和線程運行步驟可以看到三個線程並發的情況下,只生成了唯一實例。

靜態內部類

JDK版本限制,也不需要使用volatile關鍵字即可完成單例模式,核心代碼如下:


static class Singleton {

	private Singleton() {
		
	}
		
	private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }
	
    public static Singleton getInstance() {
	    return InstanceHolder.instance;  
	}
}

進行如下測試:


public class Test {
	public static void main(String[] args) {	
		for (int i = 0; i < 3; i++) {
			Thread thread = new Thread(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance().toString());
				}
				
			});
			thread.setName("Thread" + i);
			thread.start();
		}
		
	}
	
	static class Singleton {
		
		private Singleton() {
		
		}
		
		private static class InstanceHolder {
	        public static Singleton instance = new Singleton();
	    }

	    public static Singleton getInstance() {
	        return InstanceHolder.instance;  
	    }
	}
}

運行結果如下:

Thread2 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7
Thread1 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7
Thread0 com.hust.grid.leesf.mvnlearning.Test$Singleton@71f801f7

該模式可保證使用時才會初始化變量,達到延遲初始化目的。

總結

單例模式在多線程環境下不太好編寫,並且不容易重現異常,編寫時需要謹慎,在項目中遇到問題也需要多總結和記錄。

參考文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM