設(shè)計(jì)模式之單例模式

單例設(shè)計(jì)模式理解起來(lái)非常簡(jiǎn)單。一個(gè)類只允許創(chuàng)建一個(gè)對(duì)象(或者實(shí)例),那這個(gè)類就是一個(gè)單例類,這種設(shè)計(jì)模式就叫單例模式。

使用場(chǎng)景

處理資源訪問(wèn)沖突

下面的示例中如果每個(gè)類都創(chuàng)建一個(gè) Logger 實(shí)例,就可能造成日志內(nèi)容被覆蓋的情況。

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("log.txt");
    writer = new FileWriter(file, true); //true表示追加寫(xiě)入
  }

  public void log(String message) {
    writer.write(mesasge);
  }
}


public class UserController {
  private Logger logger = new Logger();

  public void login(String username, String password) {
    // ...省略業(yè)務(wù)邏輯代碼...
    logger.log(username + " logined!");
  }
}

public class OrderController {
  private Logger logger = new Logger();

  public void create(OrderVo order) {
    // ...省略業(yè)務(wù)邏輯代碼...
    logger.log("Created an order: " + order.toString());
  }
}

表示全局唯一類

如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計(jì)為單例類。比如,配置信息類,全局 ID 生成器等。

如何實(shí)現(xiàn)一個(gè)單例?

要實(shí)現(xiàn)一個(gè)單例,我們要考慮以下幾點(diǎn):

  • 構(gòu)造函數(shù)需要是 private 訪問(wèn)權(quán)限的,這樣才能避免外部通過(guò) new 創(chuàng)建實(shí)例;
  • 考慮對(duì)象創(chuàng)建時(shí)的線程安全問(wèn)題;
  • 考慮是否支持延遲加載;
  • 考慮 getInstance() 性能是否高(是否加鎖)。

餓漢式

public class Singleton {
  private static final Singleton instance = new Singleton();

  private Singleton() {}

  public static Singleton getInstance() {
    return instance;
  }
}

懶漢式

懶漢式相對(duì)于餓漢式的優(yōu)勢(shì)是支持延遲加載。但缺點(diǎn)也很明顯,因?yàn)槭褂昧?code>synchronized關(guān)鍵字導(dǎo)致這個(gè)方法的并發(fā)度很低。如果這個(gè)單例類偶爾會(huì)被用到,那這種實(shí)現(xiàn)方式還可以接受。但是,如果頻繁地用到,就會(huì)導(dǎo)致性能瓶頸,這種實(shí)現(xiàn)方式就不可取了。

public class Singleton {
  private static Singleton instance;

  private Singleton() {}

  public static synchronized Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

雙重檢測(cè)

這是一種既支持延遲加載、又支持高并發(fā)的單例實(shí)現(xiàn)方式。

public class Singleton {
  private static Singleton instance;

  private Singleton() {}

  public static Singleton getInstance() {
    if (instance == null) {
      synchronized(Singleton.class) { // 此處為類級(jí)別的鎖
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

在 java1.5 以下instance = new Singleton();有指令重排問(wèn)題,需要給instance成員變量加上volatile關(guān)鍵字,java1.5 之后不會(huì)再這個(gè)有問(wèn)題。

靜態(tài)內(nèi)部類

這種方式利用了 Java 的靜態(tài)內(nèi)部類,有點(diǎn)類似餓漢式,但又能做到了延遲加載。

當(dāng)外部類 Singleton 被加載的時(shí)候,并不會(huì)創(chuàng)建 SingletonHolder 實(shí)例對(duì)象。只有當(dāng)調(diào)用 getInstance() 方法時(shí),SingletonHolder 才會(huì)被加載,這個(gè)時(shí)候才會(huì)創(chuàng)建 instance。insance 的唯一性、創(chuàng)建過(guò)程的線程安全性,都由 JVM 來(lái)保證。所以,這種實(shí)現(xiàn)方法既保證了線程安全,又能做到延遲加載。

public class Singleton {
  private Singleton() {}

  private static class SingletonHolder{
    private static final Singleton instance = new Singleton();
  }

  public static Singleton getInstance() {
    return SingletonHolder.instance;
  }
}

枚舉

這是一種最簡(jiǎn)單的實(shí)現(xiàn)方式,基于枚舉類型的單例實(shí)現(xiàn)。這種實(shí)現(xiàn)方式通過(guò) Java 枚舉類型本身的特性,保證了實(shí)例創(chuàng)建的線程安全性和實(shí)例的唯一性。

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);

  public long getId() {
    return id.incrementAndGet();
  }
}

如何實(shí)現(xiàn)線程唯一的單例?

上面的單例類對(duì)象是進(jìn)程唯一的,一個(gè)進(jìn)程只能有一個(gè)單例對(duì)象。那如何實(shí)現(xiàn)一個(gè)線程唯一的單例呢?

假設(shè) IdGenerator 是一個(gè)線程唯一的單例類。在線程 A 內(nèi),我們可以創(chuàng)建一個(gè)單例對(duì)象 a。因?yàn)榫€程內(nèi)唯一,在線程 A 內(nèi)就不能再創(chuàng)建新的 IdGenerator 對(duì)象了,而線程間可以不唯一,所以,在另外一個(gè)線程 B 內(nèi),我們還可以重新創(chuàng)建一個(gè)新的單例對(duì)象 b。

我們通過(guò)一個(gè) ConcurrentHashMap 來(lái)存儲(chǔ)對(duì)象,其中 key 是線程 ID,value 是對(duì)象。這樣我們就可以做到,不同的線程對(duì)應(yīng)不同的對(duì)象,同一個(gè)線程只能對(duì)應(yīng)一個(gè)對(duì)象。實(shí)際上,Java 語(yǔ)言本身提供了 ThreadLocal 工具類,可以更加輕松地實(shí)現(xiàn)線程唯一單例。

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

  private static final ConcurrentHashMap<Long, IdGenerator> instances
          = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容