單例模式
顧名思義,單例模式的意思就是隻有1個執行個體的對象。就像天天上的太陽和月亮隻有一個一樣。
單例模式有很多種寫法,在這裡我們一一對比:
1.懶漢模式
//懶漢模式
public class Singleton1 {
private static Singleton1 instance;
private Singleton1() {};
public static Singleton1 getInstance()
{
if(instance == null)
{
instance = new Singleton1();
}
return instance;
}
}
懶漢模式是指在需要的時候再去建立對象。我們可以看到它的成員屬性中有一個靜态的執行個體對象,然後對外隐藏了自己的構造方法,是的我們隻能通過靜态方法getInstance()來擷取它的對象。而在getInstance()中,判斷了執行個體對象是否已經被初始化過。
但這個解法的缺點就是隻能在單線程模式下工作,在多線程模式下會失效。
我們測試一下,測試代碼如下:
public static void main(String[] args) {
//線程1嘗試擷取一個執行個體
new Thread(new Runnable() {
@Override
public void run() {
Singleton1 s1 = Singleton1.getInstance();
System.out.println(s1.hashCode());
}
}).start();
//線程2嘗試擷取一個執行個體
new Thread(new Runnable() {
@Override
public void run() {
Singleton1 s2 = Singleton1.getInstance();
System.out.println(s2.hashCode());
}
}).start();
}
而結果是不穩定的:
會出現這樣兩種情況,是以在多線程情況下不推薦使用。
2.餓漢模式
public class Singleton2
{
private static Singleton2 instance = new Singleton2();
private Singleton2() {};
public static Singleton2 getInstance()
{
return instance;
}
}
餓漢模式解決了線程安全的問題,但是我們可以看到,因為instance對象是靜态的,在類初始化之後,這個對象就已經存在了。可能我們并不需要它,但它仍然會占用記憶體。
3.加鎖模式
public class Singleton3 {
private Singleton3() {};
static Lock lock = new ReentrantLock();
private static Singleton3 instance = null;
private static Object objSingleton3 = new Object();
public static Singleton3 getInstance()
{
synchronized (objSingleton3) {
if(instance == null)
{
instance = new Singleton3();
}
}
return instance;
}
}
這樣相比懶漢模式解決了線程安全問題,也不會在不使用的情況下生成對象,但加鎖的過程也是較為耗時的。
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {};
synchronized public static Singleton4 getInstance()
{
if(instance == null)
{
instance = new Singleton4();
}
return instance;
}
}
這種方式和上面的是一樣的,就是寫法不一樣,都是效率比較低。
然後還有一種比較類似的寫法,但卻是線程不安全的,
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {};
public static Singleton4 getInstance()
{
if(instance == null)//-------①
{
synchronized (Singleton4.class) {
instance = new Singleton4();
}
}
return instance;
}
}
這裡我們隻鎖了代碼塊,但在多線程情況下,線程A可能在①處讓出cpu,然後線程B仍然可以獲得鎖,然後建立對象。當線程A重新拿到CPU的時候,還會繼續建立新的對象,這樣就不是線程安全的 了。
4.雙重檢查模式
public class Singleton4 {
private static volatile Singleton4 instance;-------③
public volatile static int a=0;
private Singleton4() {};
public static Singleton4 getInstance()
{
if(instance == null)------①
{
synchronized (Singleton4.class) {
if(instance == null)-------②
{
instance = new Singleton4();
}
}
}
return instance;
}
}
在我們進行初始化的時候,檢查了兩次instance對象是否為空。代碼①處的檢查是因為要判斷instance是否已經存在,避免不必要的上鎖。代碼②處的檢查是因為為了解決并發情況下是否在加鎖後再次确認。這種方式相較餓漢式提高了效率,在用到的時候進行初始化,節省了空間,相對于懶漢式能夠保證線程安全。而代碼③處的volatile關鍵字保證了instance對象的可見性。避免因為jvm底層指令執行順序的原因導緻出現意料之外的錯誤。
5.靜态内部類
public class Singleton5 {
private Singleton5() {};
public static Singleton5 getInstance()
{
return Innerclass.instance;
}
private static class Innerclass{
private static final Singleton5 instance = new Singleton5();
}
}
這個模式結合了餓漢式和懶漢式的優點。它隻會在我們第一次使用這個Innerclass的時候,會調用它的構造函數去建立這個屬性,如果我們不使用Innerclass,就不會去初始化instance 。
6.枚舉法
public enum Singleton6 {
Singleton6;
}
使用的時候直接Singleton6.Singleton6就可以拿到對象。而且枚舉類有且隻有私有構造器。
而且對序列化也有很好的支援。
總結一下:
單例模式三大要素:
1.私有構造函數:確定不會被外部類通路
2.自己持有,或者通過内部類持有自己類型的執行個體對象
3.對外暴露一個擷取對象的靜态方法
對比一下幾種方式:
- 懶漢模式:隻能在單線程中使用
- 餓漢模式:建立執行個體的時機不确定,可能會浪費記憶體
- 加鎖模式:線程安全,但會在一定程度上降低代碼執行的小綠
- 雙重檢驗模式:線程安全,隻在第一次建立執行個體的時候會進行加鎖,效率也比較高
- 靜态内部類:無需加鎖,簡潔直覺,僅在需要的時候建立執行個體。
- 枚舉法:簡潔友善,自動支援了序列化,但可讀性比較低。
綜上,在一般開發的時候會比較常用4/5,但也不是唯一的,在不同的場景中可以使用不同的方法才使最好的。