Java開發(fā)中為什么要使用單例模式詳解
一、什么是單例模式?
單例設(shè)計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類,這種設(shè)計模式就叫作單例設(shè)計模式,簡稱單例模式。
二、實戰(zhàn)案例一:處理資源訪問沖突
我們先來看第一個例子。在這個例子中,我們自定義實現(xiàn)了一個往文件中打印日志的 Logger 類。具體的代碼實現(xiàn)如下所示:
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類的應(yīng)用示例:
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());
}
}
看完代碼之后,先別著急看我下面的講解,你可以先思考一下,這段代碼存在什么問題。
在上面的代碼中,我們注意到,所有的日志都寫入到同一個文件 /Users/wangzheng/log.txt 中。在 UserController 和 OrderController 中,我們分別創(chuàng)建兩個 Logger 對象。在 Web 容器的 Servlet 多線程環(huán)境下,如果兩個 Servlet 線程同時分別執(zhí)行 login() 和 create() 兩個函數(shù),并且同時寫日志到 log.txt 文件中,那就有可能存在日志信息互相覆蓋的情況。
為什么會出現(xiàn)互相覆蓋呢?我們可以這么類比著理解。在多線程環(huán)境下,如果兩個線程同時給同一個共享變量加 1,因為共享變量是競爭資源,所以,共享變量最后的結(jié)果有可能并不是加了 2,而是只加了 1。同理,這里的 log.txt 文件也是競爭資源,兩個線程同時往里面寫數(shù)據(jù),就有可能存在互相覆蓋的情況。
那如何來解決這個問題呢?我們最先想到的就是通過加鎖的方式:給 log() 函數(shù)加互斥鎖(Java 中可以通過 synchronized 的關(guān)鍵字),同一時刻只允許一個線程調(diào)用執(zhí)行 log() 函數(shù)。具體的代碼實現(xiàn)如下所示:
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);
}
}
}
不過,你仔細想想,這真的能解決多線程寫入日志時互相覆蓋的問題嗎?答案是否定的。這是因為,這種鎖是一個對象級別的鎖,一個對象在不同的線程下同時調(diào)用 log() 函數(shù),會被強制要求順序執(zhí)行。但是,不同的對象之間并不共享同一把鎖。在不同的線程下,通過不同的對象調(diào)用執(zhí)行 log() 函數(shù),鎖并不會起作用,仍然有可能存在寫入日志互相覆蓋的問題。

我這里稍微補充一下,在剛剛的講解和給出的代碼中,我故意“隱瞞”了一個事實:我們給 log() 函數(shù)加不加對象級別的鎖,其實都沒有關(guān)系。因為 FileWriter 本身就是線程安全的,它的內(nèi)部實現(xiàn)中本身就加了對象級別的鎖,因此,在外層調(diào)用 write() 函數(shù)的時候,再加對象級別的鎖實際上是多此一舉。因為不同的 Logger 對象不共享 FileWriter 對象,所以,FileWriter 對象級別的鎖也解決不了數(shù)據(jù)寫入互相覆蓋的問題。
那我們該怎么解決這個問題呢?實際上,要想解決這個問題也不難,我們只需要把對象級別的鎖,換成類級別的鎖就可以了。讓所有的對象都共享同一把鎖。這樣就避免了不同對象之間同時調(diào)用 log() 函數(shù),而導(dǎo)致的日志覆蓋問題。具體的代碼實現(xiàn)如下所示:
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);
}
}
}
除了使用類級別鎖之外,實際上,解決資源競爭問題的辦法還有很多,分布式鎖是最常聽到的一種解決方案。不過,實現(xiàn)一個安全可靠、無 bug、高性能的分布式鎖,并不是件容易的事情。除此之外,并發(fā)隊列(比如 Java 中的 BlockingQueue)也可以解決這個問題:多個線程同時往并發(fā)隊列里寫日志,一個單獨的線程負責(zé)將并發(fā)隊列中的數(shù)據(jù),寫入到日志文件。這種方式實現(xiàn)起來也稍微有點復(fù)雜。
相對于這兩種解決方案,單例模式的解決思路就簡單一些了。單例模式相對于之前類級別鎖的好處是,不用創(chuàng)建那么多 Logger 對象,一方面節(jié)省內(nèi)存空間,另一方面節(jié)省系統(tǒng)文件句柄(對于操作系統(tǒng)來說,文件句柄也是一種資源,不能隨便浪費)
我們將 Logger 設(shè)計成一個單例類,程序中只允許創(chuàng)建一個 Logger 對象,所有的線程共享使用的這一個 Logger 對象,共享一個 FileWriter 對象,而 FileWriter 本身是對象級別線程安全的,也就避免了多線程情況下寫日志會互相覆蓋的問題。
按照這個設(shè)計思路,我們實現(xiàn)了 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類的應(yīng)用示例:
public class UserController {
public void login(String username, String password) {
// ...省略業(yè)務(wù)邏輯代碼...
Logger.getInstance().log(username + " logined!");
}
}
public class OrderController {
public void create(OrderVo order) {
// ...省略業(yè)務(wù)邏輯代碼...
Logger.getInstance().log("Created a order: " + order.toString());
}
}
三、實戰(zhàn)案例二:表示全局唯一類
從業(yè)務(wù)概念上,如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計為單例類。比如,配置信息類。在系統(tǒng)中,我們只有一個配置文件,當(dāng)配置文件被加載到內(nèi)存之后,以對象的形式存在,也理所應(yīng)當(dāng)只有一份。再比如,唯一遞增 ID 號碼生成器,如果程序中有兩個對象,那就會存在生成重復(fù) ID 的情況,所以,我們應(yīng)該將 ID 生成器類設(shè)計為單例。
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
// AtomicLong是一個Java并發(fā)庫中提供的一個原子變量類型,
// 它將一些線程不安全需要加鎖的復(fù)合操作封裝為了線程安全的原子操作,
// 比如下面會用到的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();
到此這篇關(guān)于Java開發(fā)中為什么要使用單例模式詳解的文章就介紹到這了,更多相關(guān)Java單例模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析Java迭代器Iterator和Iterable的區(qū)別
Java語言中,Iterator和Iterable都是用來遍歷集合類數(shù)據(jù)結(jié)構(gòu)的接口,雖然它們有很多相似的地方,但在具體實現(xiàn)中卻有著一些不同之處,本文將詳細分析它們的區(qū)別,并提供相應(yīng)的代碼示例,需要的朋友可以參考下2023-07-07
Spring websocket并發(fā)發(fā)送消息異常的解決
本文主要介紹了 Spring websocket并發(fā)發(fā)送消息異常的解決,當(dāng)多個線程同時嘗試通過 WebSocket 會話發(fā)送消息時,會拋出異常,下面就來解決一下,感興趣的可以了解一下2023-09-09
Java源碼解析ArrayList及ConcurrentModificationException
今天小編就為大家分享一篇關(guān)于Java源碼解析ArrayList及ConcurrentModificationException,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01
詳解Java創(chuàng)建多線程的四種方式以及優(yōu)缺點
這篇文章主要介紹了Java創(chuàng)建多線程的四種方式以及優(yōu)缺點,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
java中List對象列表實現(xiàn)去重或取出及排序的方法
這篇文章主要介紹了關(guān)于java中List對象列表實現(xiàn)去重或取出以及排序的方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08
帶你走進Maven的大門-最全Maven配置及集成idea工具總結(jié)
Maven項目對象模型(POM),是一個項目管理工具可以通過一小段描述信息來管理項目的構(gòu)建,報告和文檔的軟件.那我們想要在IDEA中使用Maven得進行一些配置,接下來我們具體看一下是如何配置使用的,需要的朋友可以參考下2021-06-06
springboot連接不同數(shù)據(jù)庫的寫法詳解
這篇文章主要介紹了springboot連接不同數(shù)據(jù)庫的寫法?,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04

