一、介紹:
單例模式是應用最廣的模式之一;在應用這個模式時,單例對象的類必須保證只有一個實例的存在;許多時候,整個系統(tǒng)只需要擁有一個全局對象,這樣有利于我們協(xié)調系統(tǒng)整體的行為;如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoder中有含有線程池、緩存系統(tǒng)、網(wǎng)絡請求等,很消耗資源,很消耗資源,因此,沒有理由讓他構造多個實例。這種不能自由構造對象的情況,其實就是單例模式的使用場景;
二、定義
當前進程確保一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例;
三、單例模式使用的場景
確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。例如:創(chuàng)建一個對象需要消耗的資源過多,如要訪問IO和數(shù)據(jù)庫等資源,這時就要考慮使用單例模式;
四、單例模式的關鍵點
1、類的構造函數(shù)不對外開放,一般為Private;
2、通過一個靜態(tài)方法或者枚舉返回單例類對象;
3、確保單例類對象有且只有一個,特別是在多線程的環(huán)境下;
4、確保單例類對象在反序列化時不會重新構建對象;
單例模式的七種寫法
- 餓漢式(線程安全)
類加載的時候就進行了初始化,容易浪費內存,它基于classloader 機制避免了多線程的同步問題!非懶加載
public class Singleton implements Serializable {
private static Singleton instance = new Singleton() ;
private Singleton(){}
public static Singleton getInstance(){
return instance ;
}
//防止單例對象在反序列化時重新生成對象
private Object readResolve() throws ObjectStreamException {
return instance ;
}
}
object Singleton : Serializable {
fun doSomething(){
println("do something")
}
//防止單例對象在反序列化時重新生成對象
private fun readResolve():Any{
return Singleton
}
}
- 懶漢式(線程不安全)
最簡單的單例實現(xiàn),為懶加載實現(xiàn), 但不支持多線程,容易造成線程不安全。因為沒有加鎖,嚴格來說不算單例!
/**
* 懶漢式 線程不安全
*/
public class Singleton {
private static Singleton instance ;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance ;
}
}
//懶漢式: 線程不安全
class Singleton private constructor() {
companion object{
private var mInstance : Singleton? = null
get() {
return field?: Singleton()
}
@JvmStatic
fun getInstance() : Singleton{
return requireNotNull(mInstance)
}
}
fun doSomething(){
println("do something")
}
}
- 懶漢式(方法加鎖,線程安全)
在上一種實現(xiàn)方式上,在獲取單例的方法上加鎖
synchronized關鍵字,保證單例的實現(xiàn),是懶加載實現(xiàn),能夠很好的在多線程中工作,第一次調用才初始化,避免內存浪費,但是效率很低,因為方法加鎖會影響效率!
/**
* 懶漢式 方法加鎖 線程安全
*/
public class Singleton {
private static Singleton instance ;
private Singleton(){}
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance ;
}
}
//懶漢式,方法加鎖的懶漢式,線程安全
class Singleton private constructor() : Serializable {
companion object {
private var mInstance : Singleton ? = null
get() {
return field?: Singleton()
}
@JvmStatic
@Synchronized //添加同步鎖
fun getInstance() : Singleton {
return requireNotNull(mInstance)
}
}
//防止單例對象在反序列化時生成新的對象
private fun readResolve():Any{
return Singleton.getInstance()
}
fun doSomething(){
println("do something")
}
//kotlin調用
fun test(){
Singleton.getInstance().doSomething()
}
}
- 懶漢式(雙重校驗鎖,DCL double-check locking ,線程安全)
懶加載 ,采用雙鎖檢查機制,避免在對象實例時,對象實例指令發(fā)生重排,造成對象空指針。在多線程下保存高性能,單例對象需要使用
volatile關鍵字聲明,volatile關鍵字是線程同步的輕量級實現(xiàn),能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。可在實例域需要延遲化使用。
/**
* 懶漢式 DCL 線程安全
*/
public class Singleton {
//volatile 修飾變量,防止指令重排
private static volatile Singleton instance ;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance ;
}
}
//懶漢式: DCL 線程安全
class Singleton private constructor(){
companion object{
//使用lazy屬性代理,并指定LazyThreadSafetyMode為synchronized模式保證線程安全
@JvmStatic
val instance : Singleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
Singleton()
}
}
fun doSomething(){
println("do something")
}
}
//調用
fun test(){
Singleton.instance.doSomething()
}
- 靜態(tài)內部類
能達到和DCL 一樣的功效,但實現(xiàn)更簡單。對靜態(tài)域使用延遲初始化,應使用這種方式而不是DCL。此種方式同樣利用classloader機制來保證初始化單例只有一個線程,它和餓漢式不同的是:餓漢式只要類被裝載了,那么instance 就會被實例化,而靜態(tài)內部類實現(xiàn)單例是類被裝載了,但instance不一定被初始化。因為內部類沒有別主動使用,只有通過getInstance()調用時,才會顯示裝載內部類,從而實例instance。 實現(xiàn)時考慮:想讓單例延遲加載,又不希望單例類加載時就實例化。
/**
* 靜態(tài)內部類
*/
public class Singleton {
private Singleton(){}
private static class SingletonHolder{
private static final Singleton instance = new Singleton() ;
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
//靜態(tài)內部類實現(xiàn)單例
class Singleton private constructor() {
companion object{
@JvmStatic
fun getInstance(): Singleton {
return SingletonHolder.mInstance
}
}
fun doSomething(){
println("do something")
}
//靜態(tài)內部類
private object SingletonHolder {
val mInstance = Singleton()
}
}
- CAS 模式
算是 懶漢式加鎖 的一個變種,
synchronized是一種悲觀鎖, 而CAS是樂觀鎖,相對較輕,更輕量級。
import java.util.concurrent.atomic.AtomicReference;
/**
* CAS模式 : 存在忙等的問題,可能會造成 CPU 資源的浪費
*/
public class Singleton {
private static AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>() ;
private Singleton(){}
public static final Singleton getInstance(){
while (true){
Singleton instance = INSTANCE.get() ;
if (null == instance){
INSTANCE.compareAndSet(null,new Singleton());
}
return INSTANCE.get() ;
}
}
}
- 枚舉實現(xiàn)
是多線程安全,非懶加載。沒有被廣泛使用。它很簡介,自動支持序列化機制,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實例化
/**
* 枚舉實現(xiàn)單例
*/
public enum Singleton {
//定義一個枚舉,代表Singleton的一個實例
INSTANCE ;
public void doSomething(){
System.out.println("do something");
}
//假設在外部調用
void test(){
Singleton.INSTANCE.doSomething();
}
}
//枚舉實現(xiàn)單例
enum class Singleton {
INSTANCE ;
fun doSomething(){
println("do something")
}
}
總結
單例模式是使用頻率很高的模式,但是,由于在客戶端通常沒有高并發(fā)的情況,因此,選擇哪種實現(xiàn)方法并不會有太大的影響,即使如此,出于效率考慮,一般也是使用DCL方式和內部類單例的實現(xiàn)形式;
優(yōu)點
1、單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁的創(chuàng)建和銷毀時,而且創(chuàng)建和銷毀的性能又無法優(yōu)化,單例模式的優(yōu)勢就非常明顯;
2、單例模式只生產一個實例,減少了系統(tǒng)的新能開銷,當一個對象的場所需要較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啟動時直接產生一個單例對象,然后永久駐留內存的方式來解決;
3、單例模式可以避免對資源的多重占用,例如一個寫文件操作,由于只有一個實例存在內存中,避免對一個資源文件的同時寫操作;
4、單例模式可以在系統(tǒng)設置全局的訪問點,優(yōu)化和共享資源訪問,例如,可以設計一個單例類,負責所有的數(shù)據(jù)表的映射管理;
缺點
1、單例模式一般沒有借口,擴展困難,如要擴展,除了修改代碼基本上沒有第二種途徑可以實現(xiàn);
2、單例對象如果持有Context,那么很容易引發(fā)內存泄漏,此時需要注意傳給到單例對象的Context最好是Application Context ;
3、不利于測試,與單一職責原則有沖突
什么時候使用?
- 比如生成唯一序列號的環(huán)境
- 整個項目中需要一個共享的訪問點或共享數(shù)據(jù)
- 創(chuàng)建一個對象需要消耗的資源過多
- 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境
補充
volatile 關鍵字 : 該關鍵字與內存模型有關,需要先了解內存模型
計算機在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行過程中,需要進行數(shù)據(jù)的讀取和寫入。程序運行時的臨時數(shù)據(jù)是存放在主存中(物理內存中),這就存在一個問題: 由于CPU執(zhí)行速度很快,而從內存讀取和寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度慢的多,因此如果任何時候對數(shù)據(jù)的操作都需要同內存進行交互,會大大降低指令執(zhí)行的速,所以在CPU中有了高速緩存。這樣,當程序在運行過程中,會將運算需要的數(shù)據(jù)從主存復制一份到 高速緩存中,這樣CPU進行計算時,就可以直接從高速緩存中讀取和寫入數(shù)據(jù),當運算結束后,再將高速緩存的數(shù)據(jù)刷新到主存。
在多核CPU中,每條線程可能運行在不同的CPU中,因此每個線程都有自己的高速緩存,因此,對于一個變量,在多線程運行中,可能在多個CPU中都有改變量的高速緩存,這樣對該該變量就有可能出現(xiàn)緩存不一致的問題.
并發(fā)編程中的三個問題: 原子性問題、可見性問題、有序性問題。
原子性:即一個操作或者多個操作,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就不執(zhí)行。
在java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性,要么執(zhí)行,要么不執(zhí)行。只有簡單的讀取、賦值才是原子操作(變量之間的操作就不是原子操作)。如果實現(xiàn)更大范圍的原子性,可以通過
synchronize和Lock實現(xiàn),能夠保證任一時刻只能由一個線程執(zhí)行該代碼塊,這樣就不存在原子性問題了。
可見性:當多個線程訪問一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看的到修改的值。
Java 提供了
volatile關鍵字來保證可見性,當一個變量被其修飾時,它會保存修改立即更新到主存,當其他線程需要獲取時,它會去主存中讀取新值。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖后將變量的修改刷新到主存,因此可以保證可見性。
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
Java可以通過
volatile關鍵字來保證一定的有序性,synchronized和Lock也可保證有序性
指令重排:一般來說,處理器為了提高程序的執(zhí)行效率,可能對輸入的代碼進行優(yōu)化,它不保證程序中的各個語句的執(zhí)行順序和代碼中的順序一致,但它會保證程序最終的執(zhí)行結果和代碼順序執(zhí)行的記過是一致的。
要想并發(fā)程序正確的執(zhí)行,必須保證原子性、可見性、有序性,只要一個沒有被保證,就有可能導致程序運行不正確。
Java內存模型具備一些先天的有序性,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
happens-before原則:
- 程序次序規(guī)則:一個線程內,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
- 鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作
- volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
- 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作
- 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
- 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執(zhí)行
- 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
volatile:一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義: 1) 保證了不同線程對這個變量進行操作的可見性,即一個線程改變了某個變量的值,則新值對其他線程來說時立即可見的。 2) 禁止指令重排序。
最終結果:volatile可以保證操作的可見性、有序性,但不能保證操作變量的原子性。
volatile關鍵字參考Java并發(fā)編程:volatile關鍵字解析 - Matrix海子 - 博客園 (cnblogs.com)