首先看看單例模式的寫法
首先我們先來回顧一下餓漢式單例模式:
class Singleton{private static Singleton singleton=new Singleton();private Singleton(){}public static Singleton getInstrance(){return singleton;}
}
public class Test{public static void main(String[] args){Singleton singleton=Singleton.getInstrance();}
}
??????餓漢式是在類加載的時候創建實例,故不存在線程安全問題。
虛擬機會保證一個類的<clinit>
()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>
()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>
()方法完畢。
??????不用我們手工去保證線程安全問題
public class Demo1 {public static void main(String[] args){for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton.getInstrance());}}).start();}}}
class Singleton{private static Singleton singleton=new Singleton();private Singleton(){System.out.println("我是餓漢模式");}public static Singleton getInstrance(){return singleton;}
}
---------------------------------------------------------------------------------
我是餓漢模式,創建的10個對象是同一個對象
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
cn.xiong.demo.sychnoized.singleton.Singleton@23b1870f
優點:
類加載時完成初始化,獲取對象的速度較快.
缺點:
類加載較慢.
懶漢式單例模式可以這樣寫:
class Singleton{private static Singleton singleton = null;private Singleton(){}public static Singleton getInstrance(){if(singleton==null){singleton=new Singleton;}return singleton;}
}
-----------------------------------------------------------------------
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@2e3a9cb1
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e
cn.xiong.demo.sychnoized.singleton.SingleTon2@56f2a97e //下面有一個不同于上面的對象
cn.xiong.demo.sychnoized.singleton.SingleTon2@7b69b864
??????現在我們學習多線程相關知識,就很容易能夠發現上述懶漢式單例模式為什么線程不安全了。假設開始線程0進入,判斷singleton為空,在將要創建實例時,cpu切換,
線程1又進來了,同樣singleton為空 創建了實例,這是cpu切換回來到0線程,繼續創建實例
可見,經過分析共創建了 兩個實例,還談什么單例。
怎么才能使懶漢式單例模式線程安全呢?
1.synchronized,volatile雙層鎖處理
public class Demo2 {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(SingleTon2.getInstance());}}).start();}}}class SingleTon2{private volatile static SingleTon2 singleton=null; //第二層鎖,volatile關鍵字讓內存是否可見有關private SingleTon2() {}public static SingleTon2 getInstance() {if(singleton == null) { //第一層檢查,檢查是否有引用指向對象,高并發情況下會有多個線程同時進入synchronized (SingleTon2.class) { //第一層鎖,保證只有一個線程進入,其它線程等執行完畢if(singleton ==null) //第二層檢查,那么第一個線程創建完對象釋放鎖后,volatile保證了singleton 可見,第二個線程就進不去了singleton = new SingleTon2();}}return singleton;}
}
2.靜態內部類
public class StaticInnerClassChuli {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton3.getInstance());}}).start();}}}class Singleton3{private Singleton3() {}//靜態內部類private static class SingleTonHoler{private static Singleton3 singleton3 = new Singleton3();}public static Singleton3 getInstance() {return SingleTonHoler.singleton3;}}
靜態內部類的優點是
:外部類加載時并不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不占內存。
??????只有當getInstance()方法第一次被調用時,才會去初始化INSTANCE,第一次調用getInstance()方法會導致虛擬機加載SingleTonHoler類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。
??????同上面說的餓漢模式創建靜態對象一樣,類加載時, 虛擬機會保證一個類的<clinit>
()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>
()方法完畢。
??????那么,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有著一個致命的缺點,就是傳參的問題,由于是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,所以,我們創建單例時,可以在靜態內部類與懶漢式模式里自己斟酌。
3.枚舉類的形式
public class EnumSingleChuli {public static void main(String[] args) {for(int i=0;i<10;i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println(Singleton4.INSTANCETON);System.out.println(Singleton4.getInstance());}}).start();}}}enum Singleton4{INSTANCETON;public static Singleton4 getInstance() {return Singleton4.INSTANCETON;}
}
枚舉單例模式在《Effective Java》中推薦的單例模式之一。但本人還是喜歡在餓漢模式或靜態內部類方法中選擇來用
線程通信
??????在前面我們講了很多關于同步的問題,然而在現實開發中,是需要線程之間的協作,也就是需要線程之間實現通信的。
先了解一下最經典的程序設計模式之一的生產者-消費者模型
??????日常生活中,每當我們缺少某些生活用品時,我們都會去超市進行購買,那么,你有沒有想過,你是以什么身份去的超市呢?相信大部分人都會說自己是消費者,確實如此,那么既然我們是消費者,又是誰替我們生產各種各樣的商品呢?當然是超市的各大供貨商,自然而然地也就成了我們的生產者。
??????如此一來,生產者有了,消費者也有了,那么將二者聯系起來的超市又該作何理解呢?
??????誠然,它本身是作為一座交易場所而誕生。
??????將上述場景類比到我們實際的軟件開發過程中,經常會見到這樣一幕:
代碼的某個模塊負責生產數據(供貨商),
??????而生產出來的數據卻不得不交給另一 模塊(消費者) 來對其進行處理,
在兩個程序模塊之間我們必須要有一個類似上述超市的東西來存儲數據(超市),這就抽象出了我們的 生產者/消費者模型
其中,產生數據的模塊,就形象地稱為生產者;
而處理數據的模塊,就稱為消費者;
生產者和消費者之間的中介就叫做緩沖區,一般就是一個隊列。
在生產者-消費者模型中,當隊列滿時,生產者需要等待隊列有空間才能繼續往里面放入商品,而在等待的期間內,生產者必須釋放對隊列的占用權。因為生產者如果不釋放對隊列的占用權,那么消費者就無法消費隊列中的商品,就不會讓隊列有空間,那么生產者就會一直無限等待下去。
??????因此,一般情況下,當隊列滿時,會讓生產者交出對隊列的占用權,并進入掛起狀態。然后等待消費者消費了商品,然后消費者通知生產者隊列有空間了。
??????同樣地,當隊列空時,消費者也必須等待,等待生產者通知它隊列中有商品了。