1. 程式人生 > >JAVA設計模式-單例模式(Singleton)執行緒安全與效率

JAVA設計模式-單例模式(Singleton)執行緒安全與效率

一,前言

  單例模式詳細大家都已經非常熟悉了,在文章單例模式的八種寫法比較中,對單例模式的概念以及使用場景都做了很不錯的說明。請在閱讀本文之前,閱讀一下這篇文章,因為本文就是按照這篇文章中的八種單例模式進行探索的。

  本文的目的是:結合文章中的八種單例模式的寫法,使用實際的示例,來演示執行緒安全和效率

  既然是實際的示例,那麼就首先定義一個業務場景:購票。大家都知道在春運的時候,搶票是非常激烈的。有可能同一張票就同時又成百上千的人同時在搶。這就對程式碼邏輯的要求很高了,即不能把同一張票多次出售,也不能出現票號相同的票。

  那麼,接下來我們就使用單例模式,實現票號的生成。同時呢在這個過程中利用上述文章中的八種單例模式的寫法,來實踐這八種單例模式的執行緒安全性和比較八種單例模式的效率。

  既然文章中第三種單例模式(懶漢式)是執行緒不安全的,那麼我就從這個單例模式的實現開始探索一下執行緒安全。

  因為不管是八種單例模式的實現方式的哪一種,票號的生成邏輯都是一樣的,所以,在此正式開始之前,為了更方便的編寫示例程式碼,先做一些準備工作:封裝票號生成父類程式碼。

二,封裝票號生成父類程式碼

package com.zcz.singleton;

public class TicketNumberHandler {
    //記錄下一個唯一的號碼
    private long nextUniqueNumber = 1;
    /**
     * 返回生成的號碼
     * 
@return */ public Long getTicketNumber() { return nextUniqueNumber++; } }

  票號的生成邏輯很簡單,就是一個遞增的整數,每獲取一次,就增加1。以後我們的每一種單例模式都繼承這個父類,就不用每一次都編寫這部分程式碼,做到了程式碼的重用。

  接下來就是實現第三種單例模式,探索一下會不會引起執行緒安全問題。

三,實現第三種單例模式

package com.zcz.singleton;

/**
 * 票號生成類——單利模式,即整個系統中只有唯一的一個例項
 * @author
zhangchengzi * */ public class TicketNumberHandler3 extends TicketNumberHandler{ //儲存單例例項物件 private static TicketNumberHandler3 INSTANCE; //私有化構造方法 private TicketNumberHandler3() {}; /** * 懶漢式,在第一次獲取單例物件的時候初始化物件 * @return */ public static TicketNumberHandler3 getInsatance() { if(INSTANCE == null) { try { //這裡為什麼要讓當前執行緒睡眠1毫秒呢? //因為在正常的業務邏輯中,單利模式的類不可能這麼簡單,所以例項化時間會多一些 //讓當前執行緒睡眠1毫秒 Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } INSTANCE = new TicketNumberHandler3(); } return INSTANCE; } }

  程式碼與上述文章的一模一樣,那麼接下來就開始編寫測試程式碼。

四,編寫測試程式碼

package com.zcz.singleton;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.Vector;

public class BuyTicket {    
    public static void main(String[] args) {
        // 使用者人數
        int userNumber = 10000;
        // 儲存使用者執行緒
        Set<Thread> threadSet = new HashSet();
        
        // 用於存放TicketNumberHandler例項物件
        List<TicketNumberHandler> hanlderList = new Vector();
        // 儲存生成的票號
        List<Long> ticketNumberList = new Vector();
        
        // 定義購票執行緒,一個執行緒模擬一個使用者
        for(int i=0;i<userNumber;i++) {
            Thread t = new Thread() {
                public void run() {
                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
                    hanlderList.add(handler);
                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };
            threadSet.add(t);
        }
        System.out.println("當前購票人數:"+threadSet.size()+" 人");
        
        //記錄購票開始時間
        long beginTime = System.currentTimeMillis();
        for(Thread t : threadSet) {
            //開始購票
            t.start();
        }        
        
        //記錄購票結束時間
        long entTime;
        while(true) {
            //除去mian執行緒之外的所有執行緒結果後在記錄結束時間
            if(Thread.activeCount() == 1) {
                entTime = System.currentTimeMillis();
                break;
            }
        }
        //開始統計
        System.out.println("票號生成類例項物件數目:"+new HashSet(hanlderList).size());    
        System.out.println("共出票:"+ticketNumberList.size()+"張");    
        System.out.println("實際出票:"+new HashSet(ticketNumberList).size()+"張");
        System.out.println("出票用時:"+(entTime - beginTime)+" 毫秒");
    }
}

  結合著程式碼中的註釋,相信這部分測試程式碼理解起來並不難,首先初始化10000個執行緒,相當於10000個使用者同時購票,然後啟動這10000個執行緒開始購票,結束後做統計。

  這裡對程式碼中的hanlderList和ticketNumberList進行一下說明:

  1,這連個List的作用是什麼?這兩個List是用來做統計的。

    hanlderList用來存放單例物件,然後在最後統計的部分會轉換為Set,去除重複的物件,剩餘的物件數量就是真正的單例物件數量。如果真的是但是模式的話,在最後的統計列印的時候,票號生成類例項物件數目,應該是1。

    ticketNumberList是用來存放票號的,同樣的在最後的統計部分也會轉換為Set去重,如果真的有存在重複的票號,那麼列印資訊中的實際出票數量應該小於共出票數量

  2,這兩個List為什麼使用Vector而不是ArrayList,因為ArrayList是執行緒不安全的,如果使用ArrayList,在最後的統計中ArrayList 會出現null,這樣我們的資料就不準確了。

  那麼,開始測試。

五,第三中單例模式的測試結果

  右鍵 -> Run As -> Java Application。列印結果:

當前購票人數:10000 人
票號生成類例項物件數目:19
共出票:10000張
實際出票:9751張
出票用時:1130 毫秒

  可以看到:

  票號生成類例項物件數目:19

  說明不只是有一個單例物件產生,原因在上述的文章中也做了解釋說明。同時“共出票“實際出票數量”小於“共出票”屬性,說明產生了票號相同的票。

  ok,執行緒不安全的第三種單例示例結果之後,還有7中可用的執行緒安全的實現方式,我們就從1-8的順序逐一檢測,並通過執行時間來檢測效率高低。

六,測試第一種單例模式:使用靜態屬性,並初始化單例

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler1 extends TicketNumberHandler{    
    // 餓漢式,在類載入的時候初始化物件
    private static TicketNumberHandler1 INSTANCE = new TicketNumberHandler1();
    //私有化構造方法
    private TicketNumberHandler1() {};
    /**
     * 獲取單例例項
     * @return
     */
    public static TicketNumberHandler1 getInstance() {
        return INSTANCE;
    }
}

  2,修改測試類中使用的單例  

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();               
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1093 毫秒

  跟上一次的列印結果相比對,票號生成類例項物件數目確實只有一個了,這說明第一種單例模式,在多執行緒下是可以正確使用的。

  而且,實際出票數量和共出票數量相同,也是沒有出現重複的票號的。但是真的是這樣的嗎?我麼把使用者數量調整到20000人,多執行幾次程式碼試試看,你會發現偶爾會出現下面的列印結果:

當前購票人數:20000 人
票號生成類例項物件數目:1
共出票:20000張
實際出票:19996張
出票用時:5291 毫秒

  票號生成類的例項物件一直是1,這沒問題,因為單例模式在多執行緒環境下正確執行了。

  但是實際出票數量小於了共出票數量,這說明出現了重複的票號,為什麼呢?因為我們票號的生成方法,不是執行緒安全的

public Long getTicketNumber() {
        return nextUniqueNumber++;
    }    

  程式碼中的nextUniqueNumber++是不具備原子性的,雖然看起來只有一行程式碼,但是實際上執行了三個步驟:讀取nextUniqueNumber的值,將nextUniqueNumber的值加一,將結果賦值給nextUniqueNumber。

  所以出現重複票號的原因在於:在賦值沒有結束前,有多個執行緒讀取了值。

  怎麼優化呢?最簡單的就是使用同步鎖。在getTicketNumber上新增關鍵字synchronized。

public synchronized Long getTicketNumber() {
        return nextUniqueNumber++;
    }    

  還有另外一個方法,就是使用執行緒安全的AtomicLong

package com.zcz.singleton;

import java.util.concurrent.atomic.AtomicLong;

public class TicketNumberHandler {
    private AtomicLong nextUniqueNumber = new AtomicLong();
    //記錄下一個唯一的號碼
//    private long nextUniqueNumber = 1;
    /**
     * 返回生成的號碼
     * @return
     */
    public synchronized Long getTicketNumber() {
//        return nextUniqueNumber++;
        return nextUniqueNumber.incrementAndGet();
    }    
}

  ok,解決了這裡的問題之後,我們將使用者人數,重新調整到10000人,執行10次,統計平均執行時間:1154.3毫秒

七,測試第二種單例模式:使用靜態程式碼塊

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler2 extends TicketNumberHandler {
    // 餓漢式
    private static TicketNumberHandler2 INSTANCE;
    
    //使用靜態程式碼塊,初始化物件
    static {
        INSTANCE = new TicketNumberHandler2();
    }
    //私有化構造方法
    private TicketNumberHandler2() {};
    /**
     * 獲取單例例項
     * @return
     */
    public static TicketNumberHandler2 getInstance() {
        return INSTANCE;
    }
}

  2,修改測試程式碼

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
                    hanlderList.add(handler);
                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1234 毫秒

  單例模式成功,出票數量正確,執行10次平均執行時間:1237.1毫秒

八,測試第四種單例模式:使用方法同步鎖(synchronized)

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler4 extends TicketNumberHandler {
    //儲存單例例項物件
    private static TicketNumberHandler4 INSTANCE;
    //私有化構造方法
    private TicketNumberHandler4() {};
        
        /**
         * 懶漢式,在第一次獲取單例物件的時候初始化物件
         * @return
         */
        public synchronized static TicketNumberHandler4 getInsatance() {
            if(INSTANCE == null) {
                try {
                    //這裡為什麼要讓當前執行緒睡眠1毫秒呢?
                    //因為在正常的業務邏輯中,單利模式的類不可能這麼簡單,所以例項化時間會多一些
                    //讓當前執行緒睡眠1毫秒
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                INSTANCE = new TicketNumberHandler4();
            }
            return INSTANCE;
        }
}

  2,修改測試程式碼

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
                    TicketNumberHandler handler = TicketNumberHandler4.getInsatance();
                    hanlderList.add(handler);                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1079 毫秒

單例模式成功,出票數量正確,執行10次平均執行時間:1091.86毫秒

九,測試第五種單例模式:使用同步程式碼塊

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler5 extends TicketNumberHandler {
    //儲存單例例項物件
        private static TicketNumberHandler5 INSTANCE;
        //私有化構造方法
        private TicketNumberHandler5() {};
            
            /**
             * 懶漢式,在第一次獲取單例物件的時候初始化物件
             * @return
             */
            public static TicketNumberHandler5 getInsatance() {
                if(INSTANCE == null) {
                    synchronized (TicketNumberHandler5.class) {
                        try {
                            //這裡為什麼要讓當前執行緒睡眠1毫秒呢?
                            //因為在正常的業務邏輯中,單利模式的類不可能這麼簡單,所以例項化時間會多一些
                            //讓當前執行緒睡眠1毫秒
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                        INSTANCE = new TicketNumberHandler5();
                    }
                }
                return INSTANCE;
            }
}

  2,修改測試程式碼

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler4.getInsatance();
                    TicketNumberHandler handler = TicketNumberHandler5.getInsatance();
                    hanlderList.add(handler);                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1117 毫秒

單例模式成功,出票數量正確,執行10次平均執行時間:1204.1毫秒

十,測試第六種單例模式:雙重檢查

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler6 extends TicketNumberHandler {
    //儲存單例例項物件
    private static TicketNumberHandler6 INSTANCE;
    //私有化構造方法
    private TicketNumberHandler6() {};
        
        /**
         * 懶漢式,在第一次獲取單例物件的時候初始化物件
         * @return
         */
        public static TicketNumberHandler6 getInsatance() {
            //雙重檢查
            if(INSTANCE == null) {
                synchronized (TicketNumberHandler5.class) {
                    try {
                        //這裡為什麼要讓當前執行緒睡眠1毫秒呢?
                        //因為在正常的業務邏輯中,單利模式的類不可能這麼簡單,所以例項化時間會多一些
                        //讓當前執行緒睡眠1毫秒
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    if(INSTANCE == null) {
                        INSTANCE = new TicketNumberHandler6();
                    }
                }
            }
            return INSTANCE;
        }
}

  2,修改測試程式碼

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler4.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler5.getInsatance();
                    TicketNumberHandler handler = TicketNumberHandler6.getInsatance();
                    hanlderList.add(handler);                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1041 毫秒

單例模式成功,出票數量正確,執行10次平均執行時間:1117.1毫秒

十一,測試第七種單例模式:使用靜態內部類

  1,單例程式碼

package com.zcz.singleton;

public class TicketNumberHandler7 extends TicketNumberHandler {
    //私有化構造器
    public TicketNumberHandler7() {};
    
    //靜態內部類
    private static class TicketNumberHandler7Instance{
        private static final TicketNumberHandler7 INSTANCE = new TicketNumberHandler7();
    }
    
    public static TicketNumberHandler7 getInstance() {
        return TicketNumberHandler7Instance.INSTANCE;
    }
}

  2,修改測試程式碼

Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler4.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler5.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler6.getInsatance();
                    TicketNumberHandler handler = TicketNumberHandler7.getInstance();
                    hanlderList.add(handler);                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1250 毫秒

單例模式成功,出票數量正確,執行10次平均執行時間:1184.4毫秒

十二,測試第八種單例模式:使用列舉

  1,單例程式碼

package com.zcz.singleton;

import java.util.concurrent.atomic.AtomicLong;

public enum TicketNumberHandler8 {
    INSTANCE;
    private AtomicLong nextUniqueNumber = new AtomicLong();
    //記錄下一個唯一的號碼
//    private long nextUniqueNumber = 1;
    /**
     * 返回生成的號碼
     * @return
     */
    public synchronized Long getTicketNumber() {
//        return nextUniqueNumber++;
        return nextUniqueNumber.incrementAndGet();
    }    
}

  2,修改測試程式碼

public static void main(String[] args) {
        // 使用者人數
        int userNumber = 10000;
        // 儲存使用者執行緒
        Set<Thread> threadSet = new HashSet();
        
        // 用於存放TicketNumberHandler例項物件
        List<TicketNumberHandler8> hanlderList = new Vector();
        // 儲存生成的票號
        List<Long> ticketNumberList = new Vector();
        
        // 定義購票執行緒,一個執行緒模擬一個使用者
        for(int i=0;i<userNumber;i++) {
            Thread t = new Thread() {
                public void run() {
//                    TicketNumberHandler handler = TicketNumberHandler3.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler1.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler2.getInstance();
//                    TicketNumberHandler handler = TicketNumberHandler4.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler5.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler6.getInsatance();
//                    TicketNumberHandler handler = TicketNumberHandler7.getInstance();
                    TicketNumberHandler8 handler = TicketNumberHandler8.INSTANCE;
                    hanlderList.add(handler);                    
                    Long ticketNumber = handler.getTicketNumber();
                    ticketNumberList.add(ticketNumber);
                };
            };
            threadSet.add(t);
        }
        System.out.println("當前購票人數:"+threadSet.size()+" 人");
        
        //記錄購票開始時間
        long beginTime = System.currentTimeMillis();
        for(Thread t : threadSet) {
            //開始購票
            t.start();
        }        
        
        //記錄購票結束時間
        long entTime;
        while(true) {
            //除去mian執行緒之外的所有執行緒結果後再記錄時間
            if(Thread.activeCount() == 1) {
                entTime = System.currentTimeMillis();
                break;
            }
        }
        //開始統計
        System.out.println("票號生成類例項物件數目:"+new HashSet(hanlderList).size());    
        System.out.println("共出票:"+ticketNumberList.size()+"張");    
        System.out.println("實際出票:"+new HashSet(ticketNumberList).size()+"張");
        System.out.println("出票用時:"+(entTime - beginTime)+" 毫秒");
    }

  3,測試結果

當前購票人數:10000 人
票號生成類例項物件數目:1
共出票:10000張
實際出票:10000張
出票用時:1031 毫秒

 單例模式成功,出票數量正確,執行10次平均執行時間:1108毫秒

十三,總結  

  執行緒安全就不再多說,除去第三種方式。其他的都可以。

  效率總結表:

單例模式名稱 平均十次執行時間(毫秒)
第一種(使用靜態屬性,並初始化單例) 1154.3
第二種(使用靜態程式碼塊) 1237.1
第四種(使用方法同步鎖) 1091.86
第五種(使用同步程式碼塊) 1204.1
第六種(雙重檢查) 1117.1
第七種(使用靜態內部類) 1184.4
第八種(使用列舉) 1108

  跟我預想的不同,沒有想到的是,竟然是第四種方法的效率最高,很可能跟我測試資料的數量有關係(10000個使用者)。效率的話就不多做評論了,大家有興趣的話可以自己親自試一下。別忘記告訴我測試的結果哦。

  從程式碼行數來看,使用列舉是最程式碼最少的方法了。

  ok,這篇文章到這裡就結束了,雖然在效率上沒有結論,但是,線上程安全方面是明確了的。