深入分析java單例模式
什麼是單例模式
單例模式(Singleton Pattern)是指確保一個類在任何情況下都絕對只有一個實例,並提供一個全局訪問點。單例模式屬於創建型模式
單例模式的常見寫法
一、餓漢式單例
顧名思義餓漢式單例是在類加載的時候就立即初始化,並且創建單例對象。絕對線程安全,在線程還沒出現以前就被實例化了,不可能存在訪問安全問題
優點
沒有加任何的鎖、執行效率比較高,在用戶體驗上來說,比懶漢式更好
缺點
類加載的時候就初始化,不管用與不用都佔着空間,如果項目中有大量單例對象,則可能會浪費大量內存空間
示例
package com.zwx.design.pattern.singleton.hungry;
public class HungrySingleton {
private static final HungrySingleton hungrySigleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySigleton;
}
}
或者也可以利用靜態代碼塊的方式實現餓漢式單例
package com.zwx.design.pattern.singleton.hungry;
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySigleton;
static {
hungrySigleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance(){
return hungrySigleton;
}
}
這兩種寫法都非常的簡單,也非常好理解,餓漢式適用在單例對象較少的情況
二、懶漢式單例
懶漢式單例的特點是:被外部類調用的時候內部類纔會加載
示例1(普通寫法)
package com.zwx.design.pattern.singleton.lazy;
import com.zwx.design.pattern.singleton.hungry.HungrySingleton;
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
上面的寫法是最簡單的一種懶漢式單例寫法,但是存在線程安全問題,多線程情況下會有一定機率返回多個單例對象,這明顯違背了單例對象原則,那麼如何優化上面的代碼呢?答案就是加上synchronized關鍵字
示例2(synchronized寫法)
package com.zwx.design.pattern.singleton.lazy;
import com.zwx.design.pattern.singleton.hungry.HungrySingleton;
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
示例2的寫法僅僅是在getInstance()方法上面加了synchronized關鍵字,其他地方沒有任何變化。用 synchronized 加鎖,在線程數量比較多情況下,如果CPU分配壓力上升,會導致大批量線程出現阻塞,從而導致程序運行性能大幅下降。那麼,有沒有一種更好的方式,既 兼顧線程安全又提升程序性能呢?答案是肯定的。接下來就在介紹一種雙重檢查鎖(double-checked locking)單例寫法
示例3(DCL寫法)
package com.zwx.design.pattern.singleton.lazy;
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if(null == lazySingleton){//1
synchronized (LazyDoubleCheckSingleton.class){//2
if(null == lazySingleton){//3
lazySingleton = new LazyDoubleCheckSingleton();//4
}
}
}
return lazySingleton;//5
}
}
這裏的寫法將同步放在了方法裏面的第一個非空判斷之後,這樣可以確保對象不爲空的時候不會被阻塞,但是第二個非空判斷的意義是什麼呢?我們假設線程A首先獲得鎖,進入了第3行,還沒有釋放鎖的時候,線程B又進來了,這時候因爲線程還沒有執行對象初始化,所以判空成立,會進入第2行等待獲得鎖,這時候當線程A釋放鎖之後,線程B會進入到第3行,這時候因爲第二個判空判斷對象不爲空了,所以就會直接返回,如果沒有第2個判空,這時候就會產生新的對象了,所以需要兩次判空!
大家可能注意到這裏的變量定義上加了volatile關鍵字,爲什麼呢?這是因爲DCL在可能會存在失效的情況:
第4行代碼:lazySingleton = new LazyDoubleCheckSingleton();
大致存在以下三步:
(1)、分配內存給對象
(2)、初始化對象
(3)、將初始化好的對象和內存地址建立關聯(賦值)
而這3步由於CPU指令重排序,不能保證一定按順序執行,假如線程A正在執行new的操作,第1步和第3步都執行完了,但是第2步還沒執行完,這時候線程B進入到方法中的第1行代碼,判空不成立,所以直接返回了對象,而這時候對象並沒有初始化完全,所以就會報錯了,解決這個問題的辦法就是使用volatile關鍵字,禁止指令重排序(jdk1.5之後),保證按順序執行上面的三個步驟。想要詳細瞭解volatile關鍵字是如何解決重排序問題的,可以點擊這裏。
示例4(內部類寫法)
package com.zwx.design.pattern.singleton.lazy;
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
上面的寫法巧妙的利用了內部類的特性,LazyHolder裏面的邏輯需要等到外面方法調用時才執行。
這種寫法看起來很完美,沒有加鎖,也保證了懶加載,但是這種單例模式也有問題,那就是可以被反射或者序列化破壞單例,下面我們寫一個反射破壞單例的例子
package com.zwx.design.pattern.singleton.lazy;
import java.lang.reflect.Constructor;
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws Exception {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2 = LazyInnerClassSingleton.getInstance();
System.out.println(o1 == o2);//false
}
}
上面這個結果輸出的結果爲false,說明產生了2個對象,當然,要防止反射破壞單例很簡單,我們可以把上面例子中的構造方法加一個判斷就可以了:
private LazyInnerClassSingleton(){
//防止反射攻擊
if(null != LazyHolder.LAZY){
throw new RuntimeException("不允許構造多個實例");
}
}
這樣雖然防止了反射破壞單例,但是依然可以被序列化破壞單例,下面就讓我們驗證一下序列化是如何破壞單例的!
首先對上面的類實現序列化接口
public class LazyInnerClassSingleton implements Serializable
接下來開始對單例對象類進行序列化和反序列化測試:
package com.zwx.design.pattern.singleton.lazy;
import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;
import java.io.*;
import java.lang.reflect.Constructor;
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws Exception {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("LazyInnerClassSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);//false
}catch (Exception e){
e.printStackTrace();
}
}
}
這時候輸出結果爲false,說明產生了2個對象,那麼我們應該如何防止序列化破壞單例呢?我們可以對LazyInnerClassSingleton類加上readResolve方法就可以防止序列化破壞單例
package com.zwx.design.pattern.singleton.lazy;
import java.io.Serializable;
public class LazyInnerClassSingleton implements Serializable {
private LazyInnerClassSingleton(){
//防止反射攻擊
if(null != LazyHolder.LAZY){
throw new RuntimeException("不允許構造多個實例");
}
}
//防止序列化破壞單例
private Object readResolve(){
return LazyHolder.LAZY;
}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
這是因爲JDK源碼中會檢驗一個類中是否存在一個readResolve()方法,如果存在,則會放棄通過序列化產生的對象,而返回原本的對象,也就是說,在校驗是否存在readResolve()方法前產生了一個對象,只不過這個對象會在發現類中存在readResolve()方法後丟掉,然後返回原本的單例對象,保證了單例的唯一性,這種寫法雖然保證了單例唯一,但是過程中類也是會被實例化兩次,假如創建對象的頻率增大,就意味着內存分配的開銷也隨之增大,那麼有沒有辦法從根本上解決問題呢?那麼下面就讓繼續介紹一下注冊式單例
三、註冊式單例
註冊式單例就是將每一個實例都保存到某一個地方,然後使用唯一的標識獲取實例
示例1(容器式)
package com.zwx.design.pattern.singleton.register;
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}
}
}
容器式寫法適用於創建實例非常多的情況,便於管理。但是,是非線程安全的,spring中的單例就是屬於此種寫法
示例2(枚舉式)
package com.zwx.design.pattern.singleton.register;
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
枚舉式單例是《Effective java》一書中推薦的寫法,這種寫法避免了上面的內部類寫法中存在的問題(雖然結果唯一,但是過程產生了多個實例對象),是一種效率較高的寫法
四、ThreadLocal式單例
ThreadLocal不能保證其創建的對象是全局唯一,但是能保證在單個線程中是唯一的,天生的線程安全
示例
package com.zwx.design.pattern.singleton.threadlocal;
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}
private static final ThreadLocal<ThreadLocalSingleton> singleton =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return singleton.get();
}
}
測試
package com.zwx.design.pattern.singleton.threadlocal;
import com.zwx.design.pattern.singleton.ExectorThread;
import com.zwx.design.pattern.singleton.ExectorThread3;
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
});
t1.start();
}
}
反覆測試可以發現同一個線程獲得的對象是唯一的,不同對象則不唯一
總結
單例模式可以保證內存裏只有一個實例,減少了內存開銷;可以避免對資源的多重佔用,單例模式的寫法很多,大家可以根據自己的業務需求選擇合適自己的單例方式