Java開發中為什麼要使用單例模式詳解

一、什麼是單例模式?

單例設計模式(Singleton Design Pattern)理解起來非常簡單。一個類隻允許創建一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。

二、實戰案例一:處理資源訪問沖突

我們先來看第一個例子。在這個例子中,我們自定義實現瞭一個往文件中打印日志的 Logger 類。具體的代碼實現如下所示:

public class Logger {
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    writer.write(mesasge);
  }
}

// Logger類的應用示例:
public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略業務邏輯代碼...
    logger.log(username + " logined!");
  }
}

public class OrderController {
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
    // ...省略業務邏輯代碼...
    logger.log("Created an order: " + order.toString());
  }
}

看完代碼之後,先別著急看我下面的講解,你可以先思考一下,這段代碼存在什麼問題。

在上面的代碼中,我們註意到,所有的日志都寫入到同一個文件 /Users/wangzheng/log.txt 中。在 UserControllerOrderController 中,我們分別創建兩個 Logger 對象。在 Web 容器的 Servlet 多線程環境下,如果兩個 Servlet 線程同時分別執行 login()create() 兩個函數,並且同時寫日志到 log.txt 文件中,那就有可能存在日志信息互相覆蓋的情況。

為什麼會出現互相覆蓋呢?我們可以這麼類比著理解。在多線程環境下,如果兩個線程同時給同一個共享變量加 1,因為共享變量是競爭資源,所以,共享變量最後的結果有可能並不是加瞭 2,而是隻加瞭 1。同理,這裡的 log.txt 文件也是競爭資源,兩個線程同時往裡面寫數據,就有可能存在互相覆蓋的情況

那如何來解決這個問題呢?我們最先想到的就是通過加鎖的方式:給 log() 函數加互斥鎖(Java 中可以通過 synchronized 的關鍵字),同一時刻隻允許一個線程調用執行 log() 函數。具體的代碼實現如下所示:

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    synchronized(this) {
      writer.write(mesasge);
    }
  }
}

不過,你仔細想想,這真的能解決多線程寫入日志時互相覆蓋的問題嗎?答案是否定的。這是因為,這種鎖是一個對象級別的鎖,一個對象在不同的線程下同時調用 log() 函數,會被強制要求順序執行。但是,不同的對象之間並不共享同一把鎖。在不同的線程下,通過不同的對象調用執行 log() 函數,鎖並不會起作用,仍然有可能存在寫入日志互相覆蓋的問題。

在這裡插入圖片描述

我這裡稍微補充一下,在剛剛的講解和給出的代碼中,我故意“隱瞞”瞭一個事實:我們給 log() 函數加不加對象級別的鎖,其實都沒有關系。因為 FileWriter 本身就是線程安全的,它的內部實現中本身就加瞭對象級別的鎖,因此,在外層調用 write() 函數的時候,再加對象級別的鎖實際上是多此一舉。因為不同的 Logger 對象不共享 FileWriter 對象,所以,FileWriter 對象級別的鎖也解決不瞭數據寫入互相覆蓋的問題

那我們該怎麼解決這個問題呢?實際上,要想解決這個問題也不難,我們隻需要把對象級別的鎖,換成類級別的鎖就可以瞭。讓所有的對象都共享同一把鎖。這樣就避免瞭不同對象之間同時調用 log() 函數,而導致的日志覆蓋問題。具體的代碼實現如下所示:

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public void log(String message) {
    synchronized(Logger.class) { // 類級別的鎖
      writer.write(mesasge);
    }
  }
}

除瞭使用類級別鎖之外,實際上,解決資源競爭問題的辦法還有很多,分佈式鎖是最常聽到的一種解決方案。不過,實現一個安全可靠、無 bug、高性能的分佈式鎖,並不是件容易的事情。除此之外,並發隊列(比如 Java 中的 BlockingQueue)也可以解決這個問題:多個線程同時往並發隊列裡寫日志,一個單獨的線程負責將並發隊列中的數據,寫入到日志文件。這種方式實現起來也稍微有點復雜。

相對於這兩種解決方案,單例模式的解決思路就簡單一些瞭。單例模式相對於之前類級別鎖的好處是,不用創建那麼多 Logger 對象,一方面節省內存空間,另一方面節省系統文件句柄(對於操作系統來說,文件句柄也是一種資源,不能隨便浪費)

我們將 Logger 設計成一個單例類,程序中隻允許創建一個 Logger 對象,所有的線程共享使用的這一個 Logger 對象,共享一個 FileWriter 對象,而 FileWriter 本身是對象級別線程安全的,也就避免瞭多線程情況下寫日志會互相覆蓋的問題。

按照這個設計思路,我們實現瞭 Logger 單例類。具體代碼如下所示:

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();

  private Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加寫入
  }
  
  public static Logger getInstance() {
    return instance;
  }
  
  public void log(String message) {
    writer.write(mesasge);
  }
}

// Logger類的應用示例:
public class UserController {
  public void login(String username, String password) {
    // ...省略業務邏輯代碼...
    Logger.getInstance().log(username + " logined!");
  }
}

public class OrderController {  
  public void create(OrderVo order) {
    // ...省略業務邏輯代碼...
    Logger.getInstance().log("Created a order: " + order.toString());
  }
}

三、實戰案例二:表示全局唯一類

從業務概念上,如果有些數據在系統中隻應保存一份,那就比較適合設計為單例類。比如,配置信息類。在系統中,我們隻有一個配置文件,當配置文件被加載到內存之後,以對象的形式存在,也理所應當隻有一份。再比如,唯一遞增 ID 號碼生成器,如果程序中有兩個對象,那就會存在生成重復 ID 的情況,所以,我們應該將 ID 生成器類設計為單例。

import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
  // AtomicLong是一個Java並發庫中提供的一個原子變量類型,
  // 它將一些線程不安全需要加鎖的復合操作封裝為瞭線程安全的原子操作,
  // 比如下面會用到的incrementAndGet().
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}

// IdGenerator使用舉例
long id = IdGenerator.getInstance().getId();

到此這篇關於Java開發中為什麼要使用單例模式詳解的文章就介紹到這瞭,更多相關Java單例模式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: