設計模式——單例模式
單例模式,顧名思義就是一個類只能有一個例項。單例模式根據例項的建立的時間大致可以分為兩類——餓漢式單例和懶漢式單例。
餓漢式單例
餓漢式單例,是指在類初始化的時候就建立例項,這樣做有一個好處,就是保證在獲取例項的時候可以保證執行緒安全而且還簡單,即多個執行緒獲取到的都是同一個例項。但這樣做也有一個缺點,就是即使不用例項,例項也會建立,這樣就會造成記憶體浪費。餓漢式單例的簡單實現:
// 餓漢式單例
class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
懶漢式單例
懶漢式單例,見名知意,只有在需要的時候才會建立例項,直接看程式碼:
// 懶漢式單例
class LazySingleton {
private static LazySingleton singleton;
private LazySingleton() {
}
public LazySingleton getInstance() {
if (singleton == null) {
singleton = new LazySingleton();
}
return singleton;
}
}
很顯然懶漢式單例解決了,餓漢式單例可能出現的佔用記憶體的情況,但是餓漢式單例同樣帶來了獲取單例時執行緒不安全的問題,即可能出現多個執行緒取到的例項不是同一個,最直接的解決方案就是加鎖。
class SynchronizedLazySingleton {
private static SynchronizedLazySingleton singleton;
private SynchronizedLazySingleton() {
}
public synchronized SynchronizedLazySingleton getInstance() {
if (singleton == null) {
singleton = new SynchronizedLazySingleton();
}
return singleton;
}
}
但是加鎖後會使程式碼效能變差。所以我們需要對上面的程式碼做個優化,採用volatile和synchronized配合的方式:
class DoubleLockSingleton {
// 通過volatile修飾來確保singleton的狀態改變在所有執行緒間可見
volatile private static DoubleLockSingleton singleton;
private DoubleLockSingleton() {
}
public static DoubleLockSingleton getInstance() {
if (singleton == null) {
synchronized (DoubleLockSingleton.class) {
if (singleton == null) {
singleton = new DoubleLockSingleton();
}
}
}
return singleton;
}
}
但是這樣對效能的提升有限,我們可以換一個思路,通過內部類的初始化來優化程式碼
class InnerClassSingleton {
private InnerClassSingleton() {
}
// 靜態內部類只有在使用的時候才會初始化
public static InnerClassSingleton getInstance() {
return SingletonHolder.singleton;
}
private static class SingletonHolder {
private static final InnerClassSingleton singleton = new InnerClassSingleton();
}
}
這樣就可以達到效能與執行緒安全的平衡。
註冊式單例模式
序列化會使單例失效:有時我們需要將單例序列化後寫入磁碟,但是一旦從磁碟中把單例反序列化出來,由於單例的記憶體地址變了,這樣實際上就是建立了一個新的例項,於是只有一個例項的原則就被破壞了。通過列舉類的特性來實現註冊式單例:
enum EnumSingleton {
SINGLETON;
public EnumSingleton getInstance() {
return SINGLETON;
}
}
通過列舉類來實現單例模式,可以保證序列化後得到的是同一個物件,而且列舉類的例項不能通過反射來建立。同樣我們也可以通過hash表來實現註冊式單例:
class HashMapSingleton{
private static final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
private HashMapSingleton(){
}
public static Object getInstance(String className) {
if (!map.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
map.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
return map.get(className);
}
}
但是通過hash表來實現單例在獲取單例時還是會出現執行緒安全問題。