轉 單實例的寫法

目錄

餓漢法
單線程寫法
考慮線程安全的寫法
兼顧線程安全和效率的寫法

靜態內部類法
枚舉寫法
總結
參考資料

轉載:?你真的會寫單例模式嗎——Java實現

單例模式可能是代碼最少的模式了,但是少不一定意味著簡單,想要用好、用對單例模式,還真得費一番腦筋。本文對Java中常見的單例模式寫法做了一個總結,如有錯漏之處,懇請讀者指正。

餓漢法

顧名思義,餓漢法就是在第一次引用該類的時候就創建對象實例,而不管實際是否需要創建。代碼如下:

1
2
3
4
5
6
7
public?class?Singleton {??
????private?static?Singleton = new?Singleton();
????private?Singleton() {}
????public?static?getSignleton(){
????????return?singleton;
????}
}

這樣做的好處是編寫簡單,但是無法做到延遲創建對象。但是我們很多時候都希望對象可以盡可能地延遲加載,從而減小負載,所以就需要下面的懶漢法:

單線程寫法

這種寫法是最簡單的,由私有構造器和一個公有靜態工廠方法構成,在工廠方法中對singleton進行null判斷,如果是null就new一個出來,最后返回singleton對象。這種方法可以實現延時加載,但是有一個致命弱點:線程不安全。如果有兩條線程同時調用getSingleton()方法,就有很大可能導致重復創建對象。

1
2
3
4
5
6
7
8
public?class?Singleton {
????private?static?Singleton singleton = null;
????private?Singleton(){}
????public?static?Singleton getSingleton() {
????????if(singleton == null) singleton = new?Singleton();
????????return?singleton;
????}
}

考慮線程安全的寫法

這種寫法考慮了線程安全,將對singleton的null判斷以及new的部分使用synchronized進行加鎖。同時,對singleton對象使用volatile關鍵字進行限制,保證其對所有線程的可見性,并且禁止對其進行指令重排序優化。如此即可從語義上保證這種單例模式寫法是線程安全的。注意,這里說的是語義上,實際使用中還是存在小坑的,會在后文寫到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public?class?Singleton {
????private?static?volatile?Singleton singleton = null;
??
????private?Singleton(){}
??
????public?static?Singleton getSingleton(){
????????synchronized?(Singleton.class){
????????????if(singleton == null){
????????????????singleton = new?Singleton();
????????????}
????????}
????????return?singleton;
????}???
}

兼顧線程安全和效率的寫法

雖然上面這種寫法是可以正確運行的,但是其效率低下,還是無法實際應用。因為每次調用getSingleton()方法,都必須在synchronized這里進行排隊,而真正遇到需要new的情況是非常少的。所以,就誕生了第三種寫法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public?class?Singleton {
????private?static?volatile?Singleton singleton = null;
?????
????private?Singleton(){}
?????
????public?static?Singleton getSingleton(){
????????if(singleton == null){
????????????synchronized?(Singleton.class){
????????????????if(singleton == null){
????????????????????singleton = new?Singleton();
????????????????}
????????????}
????????}
????????return?singleton;
????}???
}

這種寫法被稱為“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似多此一舉,但實際上卻極大提升了并發度,進而提升了性能。為什么可以提高并發度呢?就像上文說的,在單例中new的情況非常少,絕大多數都是可以并行的讀操作。因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,執行效率提高的目的也就達到了。

那么,這種寫法是不是絕對安全呢?前面說了,從語義角度來看,并沒有什么問題。但是其實還是有坑。說這個坑之前我們要先來看看volatile這個關鍵字。其實這個關鍵字有兩層語義。第一層語義相信大家都比較熟悉,就是可見性。可見性指的是在一個線程中對該變量的修改會馬上由工作內存(Work Memory)寫回主內存(Main Memory),所以會馬上反應在其它線程的讀取操作中。順便一提,工作內存和主內存可以近似理解為實際電腦中的高速緩存和主存,工作內存是線程獨享的,主存是線程共享的。volatile的第二層語義是禁止指令重排序優化。大家知道我們寫的代碼(尤其是多線程代碼),由于編譯器優化,在實際執行的時候可能與我們編寫的順序不同。編譯器只保證程序執行結果與源代碼相同,卻不保證實際指令的順序與源代碼相同。這在單線程看起來沒什么問題,然而一旦引入多線程,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題。

注意,前面反復提到“從語義上講是沒有問題的”,但是很不幸,禁止指令重排優化這條語義直到jdk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無法完全避免重排序所導致的問題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。

靜態內部類法

那么,有沒有一種延時加載,并且能保證線程安全的簡單寫法呢?我們可以把Singleton實例放到一個靜態內部類中,這樣就避免了靜態實例在Singleton類加載的時候就創建對象,并且由于靜態內部類只會被加載一次,所以這種寫法也是線程安全的:

1
2
3
4
5
6
7
8
9
10
11
public?class?Singleton {
????private?static?class?Holder {
????????private?static?Singleton singleton = new?Singleton();
????}
?????
????private?Singleton(){}
?????????
????public?static?Singleton getSingleton(){
????????return?Holder.singleton;
????}
}

但是,上面提到的所有實現方式都有兩個共同的缺點:

  • 都需要額外的工作(Serializable、transient、readResolve())來實現序列化,否則每次反序列化一個序列化的對象實例時都會創建一個新的實例。

  • 可能會有人使用反射強行調用我們的私有構造器(如果要避免這種情況,可以修改構造器,讓它在創建第二個實例的時候拋異常)。

枚舉寫法

當然,還有一種更加優雅的方法來實現單例模式,那就是枚舉寫法:

1
2
3
4
5
6
7
8
9
10
public?enum?Singleton {
????INSTANCE;
????private?String name;
????public?String getName(){
????????return?name;
????}
????public?void?setName(String name){
????????this.name = name;
????}
}

使用枚舉除了線程安全和防止反射強行調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。因此,Effective Java推薦盡可能地使用枚舉來實現單例。

總結

這篇文章發出去以后得到許多反饋,這讓我受寵若驚,覺得應該再寫一點小結。代碼沒有一勞永逸的寫法,只有在特定條件下最合適的寫法。在不同的平臺、不同的開發環境(尤其是jdk版本)下,自然有不同的最優解(或者說較優解)。
比如枚舉,雖然Effective Java中推薦使用,但是在Android平臺上卻是不被推薦的。在這篇Android Training中明確指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如雙重檢查鎖法,不能在jdk1.5之前使用,而在Android平臺上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語義問題,還加入了不少鎖優化,使得多線程同步的開銷降低不少)。

最后,不管采取何種方案,請時刻牢記單例的三大要點:

  • 線程安全
  • 延遲加載
  • 序列化與反序列化安全

參考資料

《Effective Java(第二版)》
《深入理解Java虛擬機——JVM高級特性與最佳實踐(第二版)》

轉載于:https://www.cnblogs.com/zhazhaQ/p/9328929.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/451930.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/451930.shtml
英文地址,請注明出處:http://en.pswp.cn/news/451930.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

網絡爬蟲--26.Scrapy中下載器中間件Downloader Middlewares的使用

文章目錄一. Downloader Middlewares二. 設置隨機請求頭三. ip代理池中間件一. Downloader Middlewares 二. 設置隨機請求頭 三. ip代理池中間件

變量名和方法名

變量名:第一個單詞的首字母小寫,后面每一個單詞的首字母大寫。如userName; 方法名:第一個單詞的首字母小寫,后面每一個單詞的首字母大寫。如setName(); 寫出讓人一眼看懂的變量名和方法名,命名應…

openfire服務器

openfire(原名Wildfire或者JiveMessenger)是由Java語言編寫的、基于XMPP協議的服務器,具有跨平臺能力,獲得了Apache2.0許可證。 openfire是基于XMPP協議的IM的服務器端的一個實現,兩個用戶想要進行通訊,首先要連接到Openfire。服…

解決eclipse配置Tomcat時找不到server選項(Mars.2也可用)

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 集成Eclipse和Tomcat時找不到server選項: 按照網上的步驟如下: 在Eclipse中,窗口(window)——首選項…

網絡爬蟲--27.csv文件的讀取和寫入

文章目錄一. csv文件二. 讀取csv文件的兩種方式三. 寫入csv文件的兩種方式一. csv文件 二. 讀取csv文件的兩種方式 import csvdef read_csv_demo1():with open(classroom1.csv,r,encodingutf-8,newline) as fp:# reader是一個迭代器reader csv.reader(fp)next(reader)for x i…

Quiver快速入門

Quiver快速入門 裝載自:https://github.com/HappenApps/Quiver/wiki/Quiver%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8Quiver 是一個程序員專用的記事本應用,可輕松混合文本、代碼、Markdown、LaTeX 到一個記事本中。提供強大的代碼編輯功能,以及…

const指針和指向常量的指針

先看下面六種寫法: 1. const int p;2. const int *p;3. int const* p;4. int * const p;5. const int * const p;6. int const * const p; 那么我們應該怎么區分上面的寫法到底是指向常量的指針還是const指針(表示指針本身是常量)呢? 一個簡便方法&#…

配置SQL Server的身份驗證方式

下面的文章來源于網絡,講的是怎樣配置SQL Server 2005登陸驗證方式,但是內容同樣適用于SQL Server 2008. 配置SQL Server的身份驗證方式 在默認情況下,SQL Server 2005 Express是采用集成的Windows安全驗證且禁用了sa登錄名。為了工作組環境下…

計算機程序設計藝術+第3卷:排序與查找(第二版)pdf

下載地址:網盤下載 《計算機程序設計藝術排序和查找(第3卷)(第2版)》內容簡介:這是對第3卷的頭一次修訂,不僅是對經典計算機排序和查找技術的最全面介紹,而且還對第1卷中的數據結構處理技術作了進一步的擴充,通盤考慮了…

數據結構與算法--5.Python實現十大排序算法

文章目錄0. 相關概念一. 冒泡排序二. 選擇排序三. 插入排序四. 希爾排序五. 快速排序六. 歸并排序七. 其他0. 相關概念 穩定:如果a原本在b前面,而ab,排序之后a仍然在b的前面。不穩定:如果a原本在b的前面,而ab&#xf…

No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK? 問題

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 maven run as --install 時出錯,提示信息如下: [ERROR] Failed to execute goal org.apache.maven.plugins:m…

spring cloud(九):各組件常用配置參數

1、Eureka的常用配置Eureka Server端eureka.server.enable-self-preservation # 設為false,關閉自我保護eureka.server.eviction-interval-timer-in-ms # 清理間隔(單位毫秒,默認是60*1000)eureka.environmentdev #指定環境eureka…

JSON與XML的區別比較

1.定義介紹 (1).XML定義 擴展標記語言 (Extensible Markup Language, XML) ,用于標記電子文件使其具有結構性的標記語言,可以用來標記數據、定義數據類型,是一種允許用戶對自己的標記語言進行定義的源語言。 XML使用DTD(document type defini…

ajax傳遞數組:屬性traditional設置

jQuery.ajaxSettings.traditional true; $.post("",function(){});轉載于:https://www.cnblogs.com/HansZimmer/p/9334006.html

Python面試題總結(9)--高級特性

文章目錄1. 函數裝飾器有什么作用?請列舉說明?2. Python 垃圾回收機制?3. 魔法函數 _call_怎么使用?4. 如何判斷一個對象是函數還是方法?5. classmethod 和 staticmethod 用法和區別6. Python 中的接口如何實現?7. Py…

I/O流講解

本文來自:曹勝歡博客專欄:http://blog.csdn.net/csh624366188 在軟件開發中,數據流和數據庫操作占據了一個很重要的位置,所以,熟悉操作數據流和數據庫,對于每一個開發者來說都是很重要的,今天就…

Spring Boot入門(9)網頁版計算器

介紹 在寫了前八篇Spring Boot項目的介紹文章后,我們已經初步熟悉了利用Spring Boot來做Web應用和數據庫的使用方法了,但是這些僅僅是官方介紹的一個例子而已。 ??本次分享將介紹筆者自己的一個項目:網頁版計算器,以這兩篇博客…

shell編程基礎(七): 處理文件命令sed與awk

一、sed(以行為單位處理文件) sed意為流編輯器(Stream Editor),在Shell腳本和Makefile中作為過濾器使用非常普遍,也就是把前一個程序的輸出引入sed的輸入,經過一系列編輯命令轉換為另一種格式輸…

數據結構與算法--6.二分查找

文章目錄一. 二分查找二. 代碼實現一:使用遞歸三. 代碼實現二:非遞歸一. 二分查找 二. 代碼實現一:使用遞歸 def binary_search(alist, item):"""二分查找:使用遞歸"""n len(alist)if n > 0:m…