單例是什麼?
是一種對象創建模式,可以確保項目中一個類只產生一個實例。
好處
對於頻繁使用的對象可以減少創建對象所花費的時間,這對於重量級對象來說,簡直是福音。由於new的減少,對系統內存使用頻率也會降低,減少GC的壓力,並縮短GC停頓時間,這也會減少Android項目的UI卡頓。
如何實現單例
1、餓漢模式
public class TestSingleton {
private static final TestSingleton testSingleton = new TestSingleton();
private TestSingleton(){
}
public static TestSingleton getInstance(){
return testSingleton;
}
}
細節我就不多寫了,大家都應該知道,構造函數爲private,用getInstance來獲取實例
2.、懶漢模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
return testSingleton;
}
}
比餓漢式的優點在於用時再加載,比較重量級的單例,就不適用與餓漢了。
3、線程安全的懶漢模式
public class TestSingleton {
private static TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
testSingleton = new TestSingleton();
}
}
return testSingleton;
}
}
可以看到的是比上面的單例多了一個對象鎖,着可以保證在創建對象的時候,只有一個線程能夠創建對象。
4、線程安全的懶漢模式-DCL雙重檢查鎖機制
public class TestSingleton {
private static volatile TestSingleton testSingleton;
private TestSingleton(){
}
public static TestSingleton getInstance(){
if(testSingleton==null){
synchronized (TestSingleton.class){
if(testSingleton==null){
testSingleton = new TestSingleton();
}
}
}
return testSingleton;
}
}
雙重檢查,同步塊加鎖機制,保證你的單例能夠在加鎖後的代碼裏判斷空,還有增加了一個volatile 關鍵字,保證你的線程在執行指令時候按順序執行。這也是市面上見的最多的單例。
敲黑板!!知識點:原子操作、指令重排。
什麼是原子操作?
簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因爲線程調度被打斷的操作。
m = 6; // 這是個原子操作
假如m原先的值爲0,那麼對於這個操作,要麼執行成功m變成了6,要麼是沒執行m還是0,而不會出現諸如m=3這種中間態——即使是在併發的線程中。
而,聲明並賦值就不是一個原子操作:
int n = 6; // 這不是一個原子操作
對於這個語句,至少有兩個操作:
- 聲明一個變量n
- 給n賦值爲6
這樣就會有一箇中間狀態:變量n已經被聲明瞭但是還沒有被賦值的狀態。
在多線程中,由於線程執行順序的不確定性,如果兩個線程都使用m,就可能會導致不穩定的結果出現。
什麼是指令重排?
簡單來說,就是計算機爲了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。
int a ; // 語句1
a = 8 ; // 語句2
int b = 9 ; // 語句3
int c = a + b ; // 語句4
正常來說,對於順序結構,執行的順序是自上到下,也即1234。
但是,由於指令重排的原因,因爲不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324。
由於語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。
也就是說,對於非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。
主要在於testSingleton = new TestSingleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
- 給 testSingleton 分配內存
- 調用 testSingleton 的構造函數來初始化成員變量,形成實例
- 將testSingleton 對象指向分配的內存空間(執行完這步 testSingleton 纔是非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 testSingleton 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
--------------------------------------------一部分的文章可能講到如上就嘎然而止了----------------------------------------
推薦後兩種
5、靜態內部類來實現單例
public class TestSingleton {
private TestSingleton(){
}
public static TestSingleton getInstance(){
return TestSingletonInner.testSingleton;
}
private static class TestSingletonInner{
static final TestSingleton testSingleton = new TestSingleton();
}
}
static 保證數據獨一份
final 初始化完成後不能被修改,線程安全。
敲黑板!!知識點:java在加載類的時候不會將其內部的靜態內部類加載,只有在使用該內部類方法時才被調用。這明顯是最好的單例,並不需要什麼鎖一類的機制。
利用了類中靜態變量的唯一性
優點:
- jvm本身機制保證線程安全。
- synchronized 會導致性能問題。
- TestSingletonInner 是私有的,除了通過TestSingleton 訪問,沒有其他訪問的可能性。
6、枚舉單例
public enum TestSingleton {
INSTANCE;
public void toSave(){
}
}
使用TestSingleton.INSTANCE.toSave();
創建枚舉實例的過程是線程安全的,所以這種寫法也沒有同步的問題。如果你要自己添加一些線程安全的方法,記得控制線程安全哦。
優點:寫法簡單/線程安全
Android中的單例實際應用
1、application
本身就是單例,生命週期爲整個程序的生命週期,可以通過這個特性,能夠用來存儲一些數據
2、單例模式引起的內存泄漏
在使用Context注意用application中的context