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

我這里稍微補(bǔ)充一下,在剛剛的講解和給出的代碼中,我故意“隱瞞”了一個(gè)事實(shí):我們給 log() 函數(shù)加不加對(duì)象級(jí)別的鎖,其實(shí)都沒(méi)有關(guān)系。因?yàn)?FileWriter 本身就是線程安全的,它的內(nèi)部實(shí)現(xiàn)中本身就加了對(duì)象級(jí)別的鎖,因此,在外層調(diào)用 write() 函數(shù)的時(shí)候,再加對(duì)象級(jí)別的鎖實(shí)際上是多此一舉。因?yàn)椴煌?Logger 對(duì)象不共享 FileWriter 對(duì)象,所以,FileWriter 對(duì)象級(jí)別的鎖也解決不了數(shù)據(jù)寫入互相覆蓋的問(wèn)題。
那我們?cè)撛趺唇鉀Q這個(gè)問(wèn)題呢?實(shí)際上,要想解決這個(gè)問(wèn)題也不難,我們只需要把對(duì)象級(jí)別的鎖,換成類級(jí)別的鎖就可以了。讓所有的對(duì)象都共享同一把鎖。這樣就避免了不同對(duì)象之間同時(shí)調(diào)用 log() 函數(shù),而導(dǎo)致的日志覆蓋問(wèn)題。具體的代碼實(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(Logger.class) { // 類級(jí)別的鎖
writer.write(mesasge);
}
}
}
除了使用類級(jí)別鎖之外,實(shí)際上,解決資源競(jìng)爭(zhēng)問(wèn)題的辦法還有很多,分布式鎖是最常聽到的一種解決方案。不過(guò),實(shí)現(xiàn)一個(gè)安全可靠、無(wú) bug、高性能的分布式鎖,并不是件容易的事情。除此之外,并發(fā)隊(duì)列(比如 Java 中的 BlockingQueue)也可以解決這個(gè)問(wèn)題:多個(gè)線程同時(shí)往并發(fā)隊(duì)列里寫日志,一個(gè)單獨(dú)的線程負(fù)責(zé)將并發(fā)隊(duì)列中的數(shù)據(jù),寫入到日志文件。這種方式實(shí)現(xiàn)起來(lái)也稍微有點(diǎn)復(fù)雜。
相對(duì)于這兩種解決方案,單例模式的解決思路就簡(jiǎn)單一些了。單例模式相對(duì)于之前類級(jí)別鎖的好處是,不用創(chuàng)建那么多 Logger 對(duì)象,一方面節(jié)省內(nèi)存空間,另一方面節(jié)省系統(tǒng)文件句柄(對(duì)于操作系統(tǒng)來(lái)說(shuō),文件句柄也是一種資源,不能隨便浪費(fèi))
我們將 Logger 設(shè)計(jì)成一個(gè)單例類,程序中只允許創(chuàng)建一個(gè) Logger 對(duì)象,所有的線程共享使用的這一個(gè) Logger 對(duì)象,共享一個(gè) FileWriter 對(duì)象,而 FileWriter 本身是對(duì)象級(jí)別線程安全的,也就避免了多線程情況下寫日志會(huì)互相覆蓋的問(wèn)題。
按照這個(gè)設(shè)計(jì)思路,我們實(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());
}
}
三、實(shí)戰(zhàn)案例二:表示全局唯一類
從業(yè)務(wù)概念上,如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那就比較適合設(shè)計(jì)為單例類。比如,配置信息類。在系統(tǒng)中,我們只有一個(gè)配置文件,當(dāng)配置文件被加載到內(nèi)存之后,以對(duì)象的形式存在,也理所應(yīng)當(dāng)只有一份。再比如,唯一遞增 ID 號(hào)碼生成器,如果程序中有兩個(gè)對(duì)象,那就會(huì)存在生成重復(fù) ID 的情況,所以,我們應(yīng)該將 ID 生成器類設(shè)計(jì)為單例。
import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
// AtomicLong是一個(gè)Java并發(fā)庫(kù)中提供的一個(gè)原子變量類型,
// 它將一些線程不安全需要加鎖的復(fù)合操作封裝為了線程安全的原子操作,
// 比如下面會(huì)用到的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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringMVC實(shí)現(xiàn)自定義類型轉(zhuǎn)換器
本篇文章主要介紹了SpringMVC實(shí)現(xiàn)自定義類型轉(zhuǎn)換器 ,詳細(xì)的介紹了自定義類型轉(zhuǎn)換器的用法和好處,有興趣的可以了解一下。2017-04-04
Java?Spring的核心與設(shè)計(jì)思想你知道嗎
這篇文章主要為大家詳細(xì)介紹了Java?Spring的核心與設(shè)計(jì)思想,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
淺談java里的EL表達(dá)式在JSP中不能解析的問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談java里的EL表達(dá)式在JSP中不能解析的問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05
JavaFX實(shí)現(xiàn)UI美觀效果代碼實(shí)例
這篇文章主要介紹了JavaFX實(shí)現(xiàn)UI美觀效果代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
Kafka是什么及如何使用SpringBoot對(duì)接Kafka(最新推薦)
這篇文章主要介紹了Kafka是什么,以及如何使用SpringBoot對(duì)接Kafka,今天我們通過(guò)一個(gè)Demo講解了在SpringBoot中如何對(duì)接Kafka,也介紹了下關(guān)鍵類?KafkaTemplate,需要的朋友可以參考下2023-11-11
Java對(duì)時(shí)間的簡(jiǎn)單操作實(shí)例
這篇文章主要介紹了Java對(duì)時(shí)間的簡(jiǎn)單操作,實(shí)例分析了針對(duì)java.util.Date的各類常見(jiàn)操作,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-01-01
SpringBoot中集成Swagger2及簡(jiǎn)單實(shí)用
使用Swagger你只需要按照它的規(guī)范去定義接口及接口相關(guān)的信息,再通過(guò)Swagger衍生出來(lái)的一系列項(xiàng)目和工具,就可以做到生成各種格式的接口文檔,以及在線接口調(diào)試頁(yè)面等等,這篇文章主要介紹了SpringBoot中集成Swagger2,需要的朋友可以參考下2023-06-06
JDBC連接Mysql的5種方式實(shí)例總結(jié)
JDBC是Java DataBase Connectivity技術(shù)的簡(jiǎn)稱,是一種可用于執(zhí)行 SQL語(yǔ)句的Java API,下面這篇文章主要給大家介紹了關(guān)于JDBC連接Mysql的5種方式,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
如何自定義hibernate validation注解示例代碼
Hibernate Validator 是 Bean Validation 的參考實(shí)現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實(shí)現(xiàn),下面這篇文章主要給大家介紹了關(guān)于如何自定義hibernate validation注解的相關(guān)資料,需要的朋友可以參考下2018-04-04

