- 1、如何高效的創建一個線程安全的單例
- 2、Java的單例模式是不是線程安全的
- 3、java單例模式出現空指針問題。
- 4、java幾種單例模式寫法
- 5、如何正確地寫出單例模式
- 6、Java的單例模式是不是線程安全的?
單例模式的概念
單例模式就是確保只有一個實例,而且自行實例化並向整個系統傳遞這個實例,這個類就稱作為單例類
單例模式最重要的一個特點就是構造方法私有化。創建單例模式分為懶漢式和餓漢式。
第一種:懶漢式(線程不安全的)
傳統的懶漢式創建單例模式,是線程不安全的
[java] view plain copy print?
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
所謂懶漢式單例模式就是在調用的時候才去創建這個實例。這種寫法的懶加載很明顯,但是缺點就是不能再多線程訪問下正常工作。
第二種:懶漢式(線程安全的)
[java] view plain copy print?
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
線程安全的方式創建單例就是在對外的創建實例方法上加上synchronized。 這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的lazy
loading,但是,遺憾的是,效率很低,99%情況下不需要同步。
第三種:餓漢式(是線程安全的)
[java] view plain copy print?
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方式基於classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到lazy
loading的效果。
第四種:靜態內部類的方式創建單例模式(static inner class)
[java] view plain copy print?
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {// 靜態內部類
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟第三種方式不同的是(很細微的差別):第三種方式是只要Singleton類被裝載了,那麼instance就會被實例化(沒有達到lazy
loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,才會顯示裝載SingletonHolder類,從而實例化singleton。想像一下,如果實例化singleton很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化instance顯然是不合適的。這個時候,這種方式相比第三種方式就顯得很合理。
第五種:雙重校驗鎖(dubble check instance)
[java] view plain copy print?
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance(){
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
/**
* 為什麼這裡會使用雙重判定呢?
*/
singleton = new Singleton();
}
}
}
return singleton;
}
}
這種事用雙重判斷來創建一個單例的方法,那麼我們為什麼要使用兩個if判斷這個對象當前是不是空的呢
?因為當有多個線程同時要創建對象的時候,多個線程有可能都停止在第一個if判斷的地方,等待鎖的釋放,然後多個線程就都創建了對象,這樣就不是單例模式了,所以我們要用兩個if來進行這個對象是否存在的判斷。
單例也不能保證100%線程安全的。解決方法就是創建實例方法中加入java關鍵字synchronized。
java語言的關鍵字synchronized,可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多只有一個線程執行這段代碼。當兩個並發線程訪問同一個對象object中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另一個線程仍然可以訪問該object中的非加鎖代碼塊。
1.首先單例模式,構造方法要私有化,private Cart (){} ;
2.private MapGoodsBean,Integer cartMap;你不分配空間給map,就會報空指針異常,要
3.實現線程安全的單例模式,不難
//雙重檢查實現單例
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4.Double-Check概念對於多線程開發者來說不會陌生,如代碼中所示,我們進行了兩次if (singleton == null)檢查,這樣就可以保證線程安全了。這樣,實例化代碼只用執行一次,後面再次訪問時,判斷if (singleton == null),直接return實例化對象。
優點:線程安全;延遲加載;效率較高。
懶漢模式
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){}public static SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}}
2. 線程安全的懶漢模式
public class SingletonDemo {
private static SingletonDemo instance;
private SingletonDemo(){}
public static synchronized SingletonDemo getInstance(){
if(instance==null){
instance=new SingletonDemo();
}
return instance;
}}
3. 餓漢模式
public class SingletonDemo {
private static SingletonDemo instance=new SingletonDemo();
private SingletonDemo(){}
public static SingletonDemo getInstance(){
return instance;
}}
4. 靜態類內部加載
public class SingletonDemo {
private static class SingletonHolder{
private static SingletonDemo instance=new SingletonDemo();
}
private SingletonDemo(){
System.out.println(“Singleton has loaded”);
}
public static SingletonDemo getInstance(){
return SingletonHolder.instance;
}}
5.雙重校驗鎖法
public class SingletonDemo {
private volatile static SingletonDemo instance;
private SingletonDemo(){
System.out.println(“Singleton has loaded”);
}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}}}
return instance;
}}
當被問到要實現一個單例模式時,很多人的第一反應是寫出如下的代碼,包括教科書上也是這樣教我們的。
1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance () {
if (instance == null ) {
instance = new Singleton();
}
return instance;
}
}
這段代碼簡單明了,而且使用了懶加載模式,但是卻存在致命的問題。當有多個線程並行調用 getInstance() 的時候,就會創建多個實例。也就是說在多線程下不能正常工作。
懶漢式,線程安全
為了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設為同步(synchronized)。
1
2
3
4
5
6
public static synchronized Singleton getInstance () {
if (instance == null ) {
instance = new Singleton();
}
return instance;
}
雖然做到了線程安全,並且解決了多實例的問題,但是它並不高效。因為在任何時候只能有一個線程調用 getInstance() 方法。但是同步操作只需要在第一次調用時才被需要,即第一次創建單例實例對象時。這就引出了雙重檢驗鎖。
雙重檢驗鎖
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 instance == null ,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了。
1
2
3
4
5
6
7
8
9
10
public static Singleton getSingleton () {
if (instance == null ) { //Single Checked
synchronized (Singleton.class) {
if (instance == null ) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
這段代碼看起來很完美,很可惜,它是有問題。主要在於 instance = new Singleton() 這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
給 instance 分配內存
調用 Singleton 的構造函數來初始化成員變量
將instance對象指向分配的內存空間(執行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
我們只需要將 instance 變量聲明成 volatile 就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton () {
if (instance == null ) {
synchronized (Singleton.class) {
if (instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}
有些人認為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操作後面會有一個內存屏障(生成的彙編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操作都先行發生於後面對這個變量的讀操作(這裡的「後面」是時間上的先後順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前後的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。
相信你不會喜歡這種複雜又隱含問題的方式,當然我們有更好的實現線程安全的單例模式的辦法。
餓漢式 static final field
這種方法非常簡單,因為單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,所以創建實例本身是線程安全的。
1
2
3
4
5
6
7
8
9
10
public class Singleton {
//類加載時就初始化
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance (){
return instance;
}
}
這種寫法如果完美的話,就沒必要在啰嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類後一開始就被初始化,即使客戶端沒有調用 getInstance()方法。餓漢式的創建方式在一些場景中將無法使用:譬如 Singleton 實例的創建是依賴參數或者配置文件的,在 getInstance() 之前必須調用某個方法設置參數給它,那樣這種單例寫法就無法使用了。
靜態內部類 static nested class
我比較傾向於使用靜態內部類的方法,這種方法也是《Effective Java》上所推薦的。
1
2
3
4
5
6
7
8
9
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance () {
return SingletonHolder.INSTANCE;
}
}
這種寫法仍然使用JVM本身機制保證了線程安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。
枚舉 Enum
用枚舉寫單例實在太簡單了!這也是它最大的優點。下面這段代碼就是聲明枚舉實例的通常做法。
1
2
3
public enum EasySingleton{
INSTANCE;
}
我們可以通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新創建新的對象。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。
總結
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。上述所說都是線程安全的實現,文章開頭給出的第一種方法不算正確的寫法
單例也不能保證100%線程安全的。解決方法就是創建實例方法中加入Java關鍵字synchronized。
Java語言的關鍵字synchronized,可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多只有一個線程執行這段代碼。當兩個並發線程訪問同一個對象object中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另一個線程仍然可以訪問該object中的非加鎖代碼塊。
原創文章,作者:LDVXE,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/127288.html