23中設計模式(1):單例模式

定義:確保一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

1.設計要點

  • 構造方法私有化。
  • 有指向自己實例的靜態私有引用。
  • 有對外提供自身實例的靜態公有方法。

2.實現方式

根據實例化對象時機的不同分爲三種:

一種是餓漢式單例,一種是懶漢式單例,還有一種是枚舉實現,它是餓漢式單例的一種特殊情況。

  • 餓漢式單例,在單例類被加載時候,就實例化一個對象交給自己的引用。

    /**
     * 餓漢式單例(可以使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class HungryStyle {
        private static HungryStyle instance = new HungryStyle();
    
        private HungryStyle() {
        }
    
        public static HungryStyle getInstance() {
            return instance;
        }
    }
    
  • 懶漢式單例,是指在調用取得實例方法的時候纔會實例化對象。其實懶漢模式的單例創建有很多種,這裏列舉推薦使用的中l兩種方式。

    雙重檢索方式

    結合了其餘方式做了改進,其他方式的缺點如將同步鎖加到getInstance()方法上,會導致速率很慢;而不進行雙重檢索(也就是不進行第二次校驗),就會有線程安全問題,假如一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。

    /**
     * 懶漢式單例(雙重檢索,推薦使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class DoubleCheckStyle {
        //volatile的使用是爲了防止指令重排。
        private static volatile DoubleCheckStyle instance = null;
    
        private DoubleCheckStyle() {
        }
    
        public static DoubleCheckStyle getInstance() {
            if (instance == null) {
                synchronized (DoubleCheckStyle.class) {
                    if (instance == null) {
                        //new對象的過程可拆解爲三個過程:
                        //1.爲新對象分配內存空間
                        //2.將變量引用的指針指向內存地址。
                        //3.實例化對象的一系列過程
                        //假如一個線程執行了1、2,還沒來得及執行3,這時爲它分配的執行時間
                        //用完了,另一個線程進來,此時因爲上一個線程執行了2,那麼它會以爲已經
                        //實例化好對象了,就開心的拿着這個單例對象執行操作去了,實際上只分配了
                        //內存空間和引用,而沒有進行實例化,所以這個線程用的時候就會拋異常。
                        instance = new DoubleCheckStyle();
                    }
                }
            }
            return instance;
        }
    
    }
    

    靜態內部類方式

    這種方式和餓漢式單例一樣,都是採用類加載機制,但是不同的是,餓漢式在類加載時進行初始化,靜態內部類方式在調用getInstance方法時纔會實例化。

    優點:兼顧了懶漢模式的內存優化(使用時才初始化)以及餓漢模式的安全性(類的靜態屬性只會在第一次加載類的時候初始化,所以JVM幫助我們保證了線程的安全性)。

    缺點:需要兩個類去完成這一實現,雖然不會創建靜態內部類的對象,但是其 Class 對象還是會被創建,而且是屬於永久帶的對象。因此創建好的單例,一旦在後期被銷燬,不能重新創建。

    /**
     * 懶漢式單例(通過靜態內部類的方式實現,推薦使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class StaticInnerStyle {
        private StaticInnerStyle() {
    
        }
    
        private static class InnerInstance {
            private final static StaticInnerStyle INSTANCE = new StaticInnerStyle();
        }
    
        public static StaticInnerStyle getInstance() {
            return InnerInstance.INSTANCE;
        }
    }
    
  • 枚舉式單例藉助JDK1.5中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。

    package cn.suvue.discipline.practice.designpattern.singleton;
    
    /**
     * 枚舉方式實現的單例
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class EnumStyle {
        private EnumStyle() {
        }
    
        private enum Singleton {
            /**
             * 單例
             */
            INSTANCE;
            private final EnumStyle instance;
    
            Singleton() {
                this.instance = new EnumStyle();
            }
    
            private EnumStyle getInstance() {
                return instance;
            }
        }
    
        public static EnumStyle getInstance() {
            return Singleton.INSTANCE.getInstance();
        }
    }
    
    

3.優點

  • 在內存中只有一個對象,節省內存空間。
  • 避免頻繁的創建銷燬對象,可以提高性能。

4.使用注意事項

  • 只能使用單例類提供的方法得到單例對象,不要使用反射,否則將會實例化一個新對象。我們的代碼在反射面前就是裸奔的,它是一種非常規操作。

  • 構造方法時私有的,因此單例類不可被繼承。

  • 多線程使用單例使用共享資源時,注意線程安全問題。

  • 防止反射對單例造成破壞的方法,因爲反射是基於構造方法拿到的實例,所以我們可以這麼改一下:

    private StaticInnerStyle() {
            if (getInstance()!=null){
                throw new RuntimeException("調用失敗");
            }
        }
    

5.單例模式在spring框架中應用

spring中使用的單例模式的懶加載,但是使用的是單例註冊表實現的,先來看一個小例子。

package cn.suvue.discipline.practice.designpattern.singleton;


import java.util.concurrent.ConcurrentHashMap;
/**
 * 註冊表實現的單例
 *
 * @author suvue
 * @date 2020/1/9
 */
public class RegisterStyle {
    private static ConcurrentHashMap<String, Object> register = new ConcurrentHashMap<String, Object>(32);

    static {
        RegisterStyle res = new RegisterStyle();
        register.put(res.getClass().getName(), res);
    }

    private RegisterStyle() {
    }

    public static RegisterStyle getInstance(String name) {
        if (name == null) {
            name = "cn.suvue.discipline.practice.designpattern.singleton.RegisterStyle";
        }
        if (register.get(name) == null) {
            try {
                register.put(name, Class.forName(name).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return (RegisterStyle) register.get(name);
    }
}

上述的代碼很簡單,實現思路大同小異,唯一的不同是用到了ConcurrentHashMap。下面我們來看一下Spring中的應用。最經典的就是BeanFactory的獲取bean的時候。

@SuppressWarnings("unchecked")
	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

        //校驗bean名是否有非法字符
		final String beanName = transformedBeanName(name);
		Object bean;

		Object sharedInstance = getSingleton(beanName);
		if (sharedInstance != null && args == null) {
			//...
            //獲取bean的實例
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}

		else {
			//...

			try {
                //獲取並檢查bean的定義
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
				checkMergedBeanDefinition(mbd, beanName, args);

				//...

				// 創建bean的實例 類定義是單例的情況.
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
                            //出錯了要銷燬bean
							destroySingleton(beanName);
							throw ex;
						}
					});
                    //獲取bean的實例
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}

                //多例情況 這裏不多分析
				else if (mbd.isPrototype()) {
				     //...
				}

				//作用域相關代碼,這裏不看它
                //...
			}
			catch (BeansException ex) {
				cleanupAfterBeanCreationFailure(beanName);
				throw ex;
			}
		}

		// Check if required type matches the type of the actual bean instance.
		if (requiredType != null && !requiredType.isInstance(bean)) {
			//校驗bean的類型是否與實際的相匹配
            //...
		}
		return (T) bean;
	}

上面是我簡化過的代碼,我們着重看下是單例情況,也就是getSingleton方法的具體實現。

/** 單例對象的緩存容器,key是bean的名稱,value是bean的實例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);


public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(beanName, "Bean name must not be null");
		synchronized (this.singletonObjects) {
			Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
				//...
				boolean newSingleton = false;
				//...
				try {
                    //這裏實際上創建一個新的對象
                    //因爲這裏的singletonFactory
                    //實際上傳進來的是createBean(beanName, mbd, args)
					singletonObject = singletonFactory.getObject();
					newSingleton = true;
				}
				catch (IllegalStateException ex) {
				    //創建實例因爲容器中已經存在了,就拋出異常,然後到容器中直接取單例對象
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						throw ex;
					}
				}
				//...
				if (newSingleton) {
                    //如果是新的實例對象,那麼就添加到容器中
					addSingleton(beanName, singletonObject);
				}
			}
			return singletonObject;
		}
	}

下面我們看看spring是怎麼往容器中放單例對象的。

protected void addSingleton(String beanName, Object singletonObject) {
		synchronized (this.singletonObjects) {
            //直接在同步代碼塊 執行put操作
            //上段代碼的getSingleton方法 用了一個synchronized
            //本段代碼中addSingleton方法 也用了一個synchronized
            //並且鎖的對象都是singletonObjects
			this.singletonObjects.put(beanName, singletonObject);
            //下面的代碼可以不用看,重要的是上面這行
			this.singletonFactories.remove(beanName);
			this.earlySingletonObjects.remove(beanName);
			this.registeredSingletons.add(beanName);
		}
	}

6.單例對象究竟會不被GC垃圾回收機制回收呢?

答案是不會

實踐出真知,我們可以通過測試代碼來進行實驗的。

class Singleton {
    //餓漢式單例,bytes數組模擬該單例對象佔了100M堆內存
    private static byte[] bytes = new byte[1024 * 1024 * 100];

    private Singleton() {
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

}

class OtherObject {
    //模擬其它一些對象,假設都佔用了虛擬機5M堆內存
    private byte[] bytes = new byte[1024 * 1024 * 5];
}


public class SingletonTest {
    public static void main(String[] args) {
        //獲取單例對象
        Singleton instance = Singleton.getInstance();
        //死循環創建對象,然後觀察虛擬機中堆內存大小的變化
        while (true) {
            new OtherObject();
        }

    }
}

我們採用jdk自帶的可視化工具jvisualvm.exe,可以在jdk根目錄下/bin/ 下找到它。

下圖中我們可以直觀的看到,GC每次回收後都剩餘了100MB的堆內存。而我們模擬的單例對象剛好佔用了100MB的內存,說明在正常情況下,Java中GC垃圾回收機制是不會回收單例對象的,除非人爲的破壞了對象與引用之間的聯接。

在這裏插入圖片描述

以上是我的一些粗鄙觀點,希望看到本博文的大神能糾正其中的錯誤!萬分感謝哦,以後我對單例有了新的認知了,會不斷來補充更新的,也希望大神們多提提意見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章