模式是脫離語言而存在的,設計模式中的單例模式在併發中非常重要,大家不要沉迷於語言和架構,需要從設計角度去思考問題。 技術是最容易被替代的,只有形成了自己的方法論和產品思維才能走得更遠。 ------ 寫在開篇前
單例模式怎麼產生的呢?
多線程操作對象是要操作不同對象還是操作同一個對象呢?
如果要操作同一個對象的話,需要保證對象的唯一性。
單例模式要解決的問題就是:實例化過程中只實例化一次。
大致的方法就是: 1)提供一個實例化的過程,也就是new方法; 2)提供返回實例對象的方法 getInstance方法。
一、單例模式分類
分類有很多種,都是不斷演化而來的,我將常用的給大家列一下,以後搬磚的時候如果能幫到大家就值得了。並且我們從線程安全、性能、懶加載 三方面來分析每個分類。
1.1 餓漢模式
加載的時候就產生實例對象,形象的成爲餓漢模式,很飢餓馬上就要產生出來對象。
代碼示例:
public class HungerySingleton {
//加載的時候就產生的實例對象
private static HungerySingleton instance=new HungerySingleton();
private HungerySingleton(){
}
//返回實例對象
public static HungerySingleton getInstance(){
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HungerySingleton.getInstance());
}).start();
}
}
}
線程安全性: 在加載的時候被實例化,所以只實例化一次,線程安全的。
性能: 性能比較好,主要影響的也就是內存的影響,長久不用佔着內存。
懶加載: 沒有延遲加載,如果成員變量較多的時候,長時間不使用會一直在內存中存在,該釋放卻釋放不了會導致內存溢出。
1.2 懶漢模式
對餓漢模式盡行了優化,採用了延遲加載,使用的時候才加載進內存。
代碼示例:
public class HoonSingleton {
private static HoonSingleton instance=null;
private HoonSingleton(){
}
public static HoonSingleton getInstance(){
if(null==instance)
instance=new HoonSingleton();
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(HoonSingleton.getInstance());
}).start();
}
}
}
線程安全性: 不安全,不能保證示例對象的唯一性,如圖所示,當2個線程都判斷爲null來實例化的時候就會產生問題。下圖展示了爲啥線程是不安全的。
懶加載:是
性能: 較好
1.3 懶漢模式+同步
Double-Check-Locking 的實現就是這樣的,實例代碼:
public class DCL {
private static DCL instance=null;
private DCL(){
}
public static DCL getInstance(){
if(null==instance)
synchronized (DCL.class){
if(null==instance)
instance=new DCL();
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
System.out.println(DCL.getInstance());
}).start();
}
}
}
線程安全性: 線程安全
性能:比較好
懶加載:是
但是這個模式下有個問題。
比如:
public class DCL {
private static DCL instance=null;
private DCL(){
//調用數據庫連接,socket連接,實例化對象。這裏面的順序就不保證了,
//有可能指令重排,將instance重排到最前面了。
conn;
socket;
instance = new DCL();
//可能指令重排成:
// instance = new DCL();
// conn;
// socket;
}
}
就是如果是建立socket或者數據庫的連接的實例化對象,容易因爲指令重排引起空指針異常。如下圖,當第一個線程實例化了以後並沒有真正連理網絡或者數據庫連接的時候,第二個線程來了,就會直接調用連接,因爲指令重排了,instance已經不爲null了,但是conn和socket還沒有建立好,所以會導致第二個出現空指針異常。這個是因爲指令重排導致的,所以大家心裏也有概念就行了。也不一定能遇到,但是遇到了至少排查起來心裏有譜。
可以利用volatile來 限制指令重排。比如用這個限制,instance之前的語句不會重排到它後面去。
private volatile static DCL instance=null;
1.4 Holder模式
聲明類的時候,成員變量中不聲明實例變量,而放到內部靜態類中。利用靜態內部類來保證同步,跟加鎖效果一樣。在初始化HolerDemo的時候並不會建立實例對象,靜態內部類,只有在被調用的時候纔會加載,把實例化放在靜態內部類中,就可以在使用的時候實例化,實現了懶加載。
該模式結合了餓漢模式和懶漢模式,懶加載還不用加鎖。目前來說使用最廣泛的一種方式了。
public class HolderDemo {
private HolderDemo(){
}
//靜態內部類只有在調用的時候纔會加載
private static class Holder{
private static HolderDemo instance=new HolderDemo();
}
//懶加載
public static HolderDemo getInstance(){
return Holder.instance;
}
}
線程安全性:線程安全,因爲靜態類實例化的時候只能實例化一次
懶加載:是
性能:好,因爲沒有加鎖,不會串行化,性能更好。
下面的方式更加聰明,大家不一定見到過。
1.5 枚舉模式
這個是在Effective Java一書中作者大力推薦的一種方法。如下實例,
枚舉裏面的常量,在加載的時候只實例化一次。這樣就實現了只加載一次,但是沒有懶加載,我們將holder模式的思想引入進來。
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
枚舉+Holder 實現懶加載
這個大家看着是不是有點懵逼,需要結合上面的枚舉和holder在來理解。多看幾遍自己在運行下。
因爲枚舉類型裏面的常量是可以直接調用枚舉裏面的方法和變量的。INSTANCE就是EnumHolder類型。在調用的時候才實例化。大家好好理解下。
public class EnumSingletonDemo {
private EnumSingletonDemo(){
}
//內部類 枚舉
//懶加載,在調用的時候纔會加載。
private enum EnumHolder{
//類型就是EnumHoler類型
INSTANCE;
private EnumSingletonDemo instance=null;
EnumHolder(){
instance=new EnumSingletonDemo();
}
}
//懶加載
public static EnumSingletonDemo getInstance(){
return EnumHolder.INSTANCE.instance;
}
}
最後2種 模式的使用時最廣泛的,大家一定要理解了,這樣寫的代碼才能牛逼。
我曾經在一個創業公司裏面實習見過一個哥們寫的代碼,非常NB。可惜跟他共事了幾個月就離開了。 他的代碼就是這種風格的,非常優雅。還是那句話,架構這些東西年年出新的。很容易被取代。設計思想和基礎只是纔是立身之本。