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ù),而導致的日志覆蓋問題。具體的代碼實現(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ā)隊列里寫日志,一個單獨的線程負責將并發(fā)隊列中的數(shù)據(jù),寫入到日志文件。這種方式實現(xiàn)起來也稍微有點復雜。
相對于這兩種解決方案,單例模式的解決思路就簡單一些了。單例模式相對于之前類級別鎖的好處是,不用創(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)中,我們只有一個配置文件,當配置文件被加載到內(nèi)存之后,以對象的形式存在,也理所應(yīng)當只有一份。再比如,唯一遞增 ID 號碼生成器
,如果程序中有兩個對象,那就會存在生成重復 ID 的情況,所以,我們應(yīng)該將 ID 生成器類
設(shè)計為單例。
import java.util.concurrent.atomic.AtomicLong; public class IdGenerator { // AtomicLong是一個Java并發(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)文章
SpringMVC實現(xiàn)自定義類型轉(zhuǎn)換器
本篇文章主要介紹了SpringMVC實現(xiàn)自定義類型轉(zhuǎn)換器 ,詳細的介紹了自定義類型轉(zhuǎn)換器的用法和好處,有興趣的可以了解一下。2017-04-04Kafka是什么及如何使用SpringBoot對接Kafka(最新推薦)
這篇文章主要介紹了Kafka是什么,以及如何使用SpringBoot對接Kafka,今天我們通過一個Demo講解了在SpringBoot中如何對接Kafka,也介紹了下關(guān)鍵類?KafkaTemplate,需要的朋友可以參考下2023-11-11如何自定義hibernate validation注解示例代碼
Hibernate Validator 是 Bean Validation 的參考實現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實現(xiàn),下面這篇文章主要給大家介紹了關(guān)于如何自定義hibernate validation注解的相關(guān)資料,需要的朋友可以參考下2018-04-04