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

JAVA設計模式-單例模式(Singleton)線程安全與效率

保存 ring 使用方法 部分 rac cheng 原因 cts 要求

一,前言

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

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

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

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

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

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

二,封裝票號生成父類代碼

復制代碼
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,這篇文章到這裏就結束了,雖然在效率上沒有結論,但是,在線程安全方面是明確了的。

JAVA設計模式-單例模式(Singleton)線程安全與效率