1. 程式人生 > >Java多執行緒程式設計筆記10:單例模式

Java多執行緒程式設計筆記10:單例模式

立即載入:“餓漢模式”

立即載入就是指使用類的時候已經將物件建立完畢,常見的實現方法就是直接new例項化。也就是在呼叫方法前,例項就被建立了。示例程式碼如下所示:

class MyObject {
    private static MyObject myObject=new MyObject();
    private MyObject(){}
    public static MyObject getInstance(){
        //如果還有其他程式碼,存線上程安全問題
        return myObject;
    }
}
class MyThread extends
Thread
{ @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } } public class Run { public static void main(String[] args) { MyThread t1=new MyThread(); MyThread t2=new MyThread(); MyThread t3=new MyThread(); t1.start(); t2.start(); t3.start(); } } 複製程式碼

執行結果如下:

58615885
58615885
58615885
複製程式碼

可以發現,實現了單例模式,因為多個執行緒得到的例項的hashCode是一樣的。

延遲載入:“懶漢模式”

延遲載入就是在呼叫getInstance()方法時例項才被建立,常見的方法就是在getInstance()方法中進行new例項化。實現程式碼如下:

class MyObject {
    private static MyObject myObject;
    private MyObject(){}
    public static MyObject getInstance(){
        if
(myObject==null){ myObject=new MyObject(); } return myObject; } } class MyThread extends Thread{ @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } } public class Run { public static void main(String[] args) { MyThread t1=new MyThread(); MyThread t2=new MyThread(); MyThread t3=new MyThread(); t1.start(); t2.start(); t3.start(); } } 複製程式碼

但是由於在getInstance()中,存在多條語句,因此可能存線上程安全問題。執行結果也顯示了這一點:

2041531420
1348345633
1348345633
複製程式碼

甚至,當getInstance()中,有更多的語句,會出現不同的三個物件,在if(myObject==null)語句塊中加入Thread.sleep(3000),執行結果如下所示:

218620763
58615885
712355351
複製程式碼

解決方案:DCL

如果使用synchronized關鍵字,對整個getInstance()上鎖或者對整個if語句塊加鎖,會存在效率問題。

最終採用了DCL(Double-Check Locking)雙檢查鎖機制,也是大多數多執行緒結合單例模式使用的解決方案。第一層主要是為了避免不必要的同步,第二層判斷則是為了在null情況下才建立例項。

public class MyObject {
    private static MyObject myObject;

    private MyObject() {
    }

    public static MyObject getInstance() {
        if (myObject == null) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}
複製程式碼

測試結果,得到的是相同的hashcode。

靜態內建類

public class MyObject{
    private static class MyObjectHandler{
        private static MyObject myObject=new MyObject();
    }

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return MyObjectHandler.myObject;
    }
} 
複製程式碼

採用靜態內建類的方法,是執行緒安全的。

使用static程式碼塊

靜態程式碼塊的程式碼再使用類的時候就已經執行了,所以可以應用靜態程式碼塊的這個特性來實現單例設計模式。

public class MyObject {
    private static MyObject myObject=null;

    static{myObject=new MyObject();}

    private MyObject() {
    }

    public static MyObject getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return myObject;
    }
}
複製程式碼

使用enum列舉資料型別

使用列舉類時,和靜態程式碼塊的特性相似,構造方法會被自動呼叫。列舉在經過javac的編譯之後,會被轉換成形如public final class T extends Enum的定義。也就是說,我們定義的一個列舉,在第一次被真正用到的時候,會被虛擬機器載入並初始化,而這個初始化過程是執行緒安全的。而我們知道,解決單例的併發問題,主要解決的就是初始化過程中的執行緒安全問題。

所以,由於列舉的以上特性,列舉實現的單例是天生執行緒安全的。同時,列舉可解決反序列化會破壞單例的問題。

enum MyObject{
    INSTANCE;
}
複製程式碼

SimpleDataFormat

SimpleDataFormat使用了單例模式,具有執行緒安全問題。SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個執行緒建立獨立的格式例項。如果多個執行緒同時訪問一個格式,則它必須保持外部同步。

解決方案1:需要的時候建立新例項

public class DateUtil {
    
    public static  String formatDate(Date date)throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }
    
    public static Date parse(String strDate) throws ParseException{
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(strDate);
    }
}

複製程式碼

在需要用到SimpleDateFormat 的地方新建一個例項,不管什麼時候,將有執行緒安全問題的物件由共享變為區域性私有都能避免多執行緒問題,不過也加重了建立物件的負擔。在一般情況下,這樣其實對效能影響比不是很明顯的。

解決方案2:同步SimpleDateFormat物件

public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}
複製程式碼

當執行緒較多時,當一個執行緒呼叫該方法時,其他想要呼叫此方法的執行緒就要block,多執行緒併發量大的時候會對效能有一定的影響。

解決方案3:使用ThreadLocal

public class ConcurrentDateUtil {

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}
複製程式碼

使用ThreadLocal, 也是將共享變數變為獨享,執行緒獨享肯定能比方法獨享在併發環境中能減少不少建立物件的開銷。如果對效能要求比較高的情況下,一般推薦使用這種方法。