前言
這篇 博客是跑更問底的學習單例模式,看了本博客,對於一般的面試官,你都可以手撕了,但是大神級別的面試官,後邊還會補充。
一、手寫單例模式
1、餓漢式
public class Singleton {
// 構造方法私有化,其他類就不能通過new的方式來創造對象
private Singleton(){
}
// 內部提供一個當前的實例,必須要靜態化,因爲下面的靜態方法要調用
private static Singleton singleton=new Singleton();
// 提供公共的靜態方法,返回當前類的對象,外部類調用的唯一路徑
public static Singleton getInstance(){
return singleton;
}
}
2、懶漢式
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
3、對比分析
首先餓漢式,就是迫不及待的new 出一個對象,然後不管你調不調用,我都要new 出一個對象,這樣雖說是線程安全的,但是對象加載的時間長,耗費內存。 懶漢式,因爲它是懶加載,什麼時候用,什麼時候new 對象,延時了對象的創建,節省內存空間,但是它是線程不安全的。如果不知道爲啥是線程不安全的,還請看我之前寫的多線程的系列博客。
4、jdk中單例模式應用舉例
jdk中的RunTime 就是餓漢式的 ,如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
二、餓漢式線程不安全的解決方式
(1)synchronized關鍵字實現線程同步
public class Singleton {
private Singleton() {
System.out.println("hahahaha");
}
private static Singleton singleton = null;
public static Singleton getInstance() {
synchronized (Singleton.class){
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
}
上面的代碼效率稍差,先不提synchronized()重量級鎖的事情,是因爲當singleton不是null的時候,其他線程每次進來的時候都要去判斷有沒有Singleton.class這個鎖,效率差在了這裏,我們稍微改進一下,用雙端檢索機制(DCL (double check lock))進行修改.
(2) DCL 方式提高效率
public class Singleton {
private Singleton() {
System.out.println("hahahaha");
}
private static Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
到這裏你就覺得完美了,重點來了,上面的雙端監測的方式也不一定是線程安全的,因爲還有指令重排的存在。這會導致某一個線程執行到第一次檢測,讀取到的singleton不爲null時,singleton的引用對象可能沒有完成初始化
因爲singleton=new Singleton();可以分爲三步
僞代碼
memory=allocate(); //1、分配對象內存空間
singleton(memory);//2、初始化對象
singleton=memory;//3、設置singleton指向剛分配的內存地址,此時instance!=null
步驟2和步驟3不存在數據依賴關係.而且無論重排前還是重排後程序執行的結果在單線程中並沒有改變,因此這種重排優化是允許的.
memory=allocate();//1.分配對象內存空間
singleton=memory;//3.設置singleton指向剛分配的內存地址,此時instance!=null 但對象還沒有初始化完.
singleton(memory);//2.初始化對象
由上可知,當一條線程訪問singleton不爲null時,由於存在singleton實例未完成初始化的可能性,此時就造成了線程安全問題。
既然是指令重排導致的問題,我們前邊介紹了volatile關鍵字的作用了, 看一看這篇博客 https://blog.csdn.net/jerry11112/article/details/106870835
,其中volatile可以保證原子性,禁止指令重排,所以我們加上volatile關鍵字就好
public class Singleton {
private Singleton() {
}
private static volatile Singleton singleton = null;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
三、單例模式——應用場景
1、應用程序的日誌應用,一般都使用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只有一個實例去操作,否則內容不好追加。
2、數據庫連接池
3、網站計數器
4、Spring中的單例模式,Spirng bean有一個屬性爲scope 其中默認就是singleton 就是單例模式