一、餓漢式(靜態常量)
這種餓漢式的單例模式構造的步驟如下:
- 構造器私有化;(防止用new來得到物件例項)
- 類的內部建立物件;(因為1,所以2)
- 向外暴露一個靜態的公共方法;(getInstance)
示例:
class Singleton{
//1私有化構造方法
private Singleton(){
}
//2建立物件例項
private final static Singleton instance = new Singleton();
//3對外提供公有靜態方法
public static Singleton getInstance(){
return instance;
}
}
這樣的話,獲取物件就不能通過 new 的方式,而要通過 Singleton.getInstance();並且多次獲取到的都是同一個物件。
使用靜態常量的餓漢式寫法實現的單例模式的優缺點:
優點:
簡單,類裝載的時候就完成了例項化,避免了多執行緒同步的問題。
缺點:
類裝載的時候完成例項化,沒有達到 Lazy Loading (懶載入)的效果,如果從始至終都沒用過這個例項呢?那就會造成記憶體的浪費。(大多數的時候,呼叫getInstance方法然後類裝載,是沒問題的,但是導致類裝載的原因有很多,可能有其他的方式或者靜態方法導致類裝載)
總結:
如果確定會用到他,這種寫是沒問題的,但是儘量避免記憶體浪費。
二、餓漢式(靜態程式碼塊)
和上一種用靜態常量的方法類似,是把建立例項的過程放在靜態程式碼塊裡。
class Singleton{
//1同樣私有化構造方法
private Singleton(){
}
//2建立物件例項
private static Singleton instance;
//在靜態程式碼塊裡進行單例物件的建立
static {
instance = new Singleton();
}
//3提供靜態方法返回例項物件
public static Singleton getInstance() {
return instance;
}
}
優缺點:和上一種靜態常量的方式一樣;
原因:實現本來就是和上面的一樣,因為類裝載的時候一樣馬上會執行靜態程式碼塊中的程式碼。
三、懶漢式(執行緒不安全)
上面的兩種餓漢式,都是一開始類載入的時候就建立了例項,可能會造成記憶體浪費。
懶漢式的寫法如下:
class Singleton{
private static Singleton instance;
private Singleton(){
}
//提供靜態公有方法,使用的時候才建立instance
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
也就是說,同樣是 1) 私有構造器;2) 類的內部建立例項;3) 向外暴露獲取例項方法。這三個步驟。
但是懶漢式的寫法,將建立的程式碼放在了 getInstance 裡,並且只有第一次的時候會建立,這樣的話,類載入的過程就不會建立例項,同時也保證了建立只會有一次。
優點:
起到了Lazy Loading 的作用
缺點:
但是隻能在單執行緒下使用。如果一個執行緒進入了 if 判斷,但是沒來得及向下執行的時候,另一個執行緒也通過了這個 if 語句,這時候就會產生多個例項,所以多執行緒環境下不能使用這種方式。
結論:
實際開發不要用這種方式。
四、懶漢式(執行緒安全,同步方法)
因為上面說了主要的問題,就在於 if 的執行可能不同步,所以解決的方式也很簡單。
class Singleton{
private static Singleton instance;
private Singleton(){
}
//使用的時候才建立instance,同時加入synchronized同步程式碼,解決執行緒不安全問題
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
只要在獲取例項的靜態方法上加上 synchronized 關鍵字,同步機制放在getInstance方法層面,就 ok。
優點:
保留了單例的性質的情況下,解決了執行緒不安全的問題
缺點:
效率太差了,每個執行緒想要獲得類的例項的時候都呼叫 getInstance 方法,就要進行同步。
然而這個方法本身執行一次例項化程式碼就夠了,後面的想要獲得例項,就應該直接 return ,而不是進行同步。
結論:
實際開發仍然不推薦。
五、懶漢式(同步程式碼塊)
這種寫法是基於對上一種的思考,既然在方法層面效率太差,那直接在例項化的語句上加 synchronized 來讓他同步,是不是就能解決效率問題呢?
class Singleton{
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized( Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
事實上,這種方法,讓 synchronized 關鍵字放入方法體裡,又會導致可能別的執行緒同樣進入 if 語句,回到了第三種的問題,所以來不及同步就會產生執行緒不安全的問題。
結論:不可用
六、 雙重檢查Double Check
使用 volatile 關鍵字,讓修改值立即更新到主存,相當於輕量級的synchronized。
然後在下面的例項化過程裡採用 double check,也就是兩次判斷。
class Singleton{
private static volatile Singleton instance;
private Singleton(){
}
//雙重檢查
public static Singleton getInstance(){
//第一次檢查
if(instance == null){
synchronized (Singleton.class){
//第二次檢查
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
可以回想一下:
4 的懶漢式同步方法寫法裡,getInstance方法是用了synchronized修飾符,所以雖然解決了 lazy loading 的問題,執行緒也安全,但是同步起來會很慢。
而 5 的懶漢式同步程式碼塊寫法,將 synchronized 修飾符加到內部的程式碼塊部分,又會導致執行緒安全直接失效,因為可能大家都同時進入了 getInstance 方法。
所以雙檢查的方法,仍然採用 5 的寫法,將程式碼塊用 synchronized 修飾符修飾,同時,在這個內部,再加上第二重檢查,這樣,執行緒安全的同時,保證了後面的執行緒會先進行 if 的判斷而不進入程式碼塊,這樣就同時達到了效率的提升。
優點:
double-check是多執行緒開發裡經常用到的,滿足了我們需要的執行緒安全&&避免反覆進行同步的效率差&&lazy loading。
結論:推薦使用。
七、靜態內部類
靜態內部類:用static修飾的內部類,稱為靜態內部類,完全屬於外部類本身,不屬於外部類某一個物件,外部類不可以定義為靜態類,Java中靜態類只有一種,那就是靜態內部類。
class Singleton{
//構造器私有化
private Singleton(){
}
//一個靜態內部類,裡面有一個靜態屬性,就是例項
private static class SingletonInstance{
private static final Singleton instance = new Singleton();
}
//靜態的公有方法
public static Singleton getInstance(){
return SingletonInstance.instance;
}
}
核心:
- 靜態內部類在外部類裝載的時候並不會執行,也就是滿足了 lazy loading;
- 呼叫getInstance的時候會取屬性,此時才載入靜態內部類,而 jvm 底層的類裝載機制是執行緒安全的,所以利用 jvm 達到了我們要的執行緒安全;
- 類的靜態屬性保證了例項化也只會進行一次,滿足單例。
結論:推薦。
八、列舉
將單例的類寫成列舉型別,直接只有一個Instance變數。
enum Singleton{
instance;
public void sayOk(){
System.out.println("ok");
}
}
呼叫的時候也不用new,直接用Singleton.instance,拿到這個屬性。(一般INSTANCE寫成大寫)
優點:
滿足單例模式要的特點,同時還能夠避免反序列化重新建立新的物件。
這種方法是effective java作者提供的方式。
結論:推薦。
九、總結
單例模式使用的場景是:
需要頻繁建立和銷燬的物件、建立物件耗時過多或耗資源太多(重型物件)、工具類物件、頻繁訪問資料庫或者檔案的物件(資料來源、session工廠等),都應用單例模式去實現。
因為單例模式保證了系統記憶體中只存在該類的一個物件,所以能節省資源,提高效能,那麼對外來說,單例的類都不能再通過 new 去建立了,而是採用類提供的獲取例項的方法。
上面的八種寫法裡面:餓漢式兩種基本是一樣的寫法,懶漢式三種都有問題,以上物種的改進就是雙重檢查,另闢蹊徑的是靜態內部類和列舉。
所以,單例模式推薦的方式有四種:
- 餓漢式可用(雖然記憶體可能會浪費);
- 雙重檢查;
- 靜態內部類;
- 列舉。
十、單例模式在JDK裡的應用
Runtime類就是一個單例模式的類,並且可以看到,他是採用我們所說的第一種方式,即餓漢式(靜態常量的方式)
- 私有構造器;
- 靜態常量,類的內部直接將類例項化;
- 提供公有的靜態方法。