單例模式

概念

單例模式(Singleton Pattern)屬於創建型模式。通過單例模式的方法創建的類在一個JVM中只有一個實例。該類負責創建自己的對象,同時確保只有一個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

特點

  • 類構造器私有
  • 持有自己類型的私有靜態屬性
  • 對外提供獲取實例的靜態方法

示例如下:

public class Singleton {  
    //持有私有靜態實例,防止被引用
    private static final Singleton instance = new Singleton();
    //私有構造方法,防止被實例化 
    private Singleton (){}  
    //靜態方法,創建實例
    public static Singleton getInstance() {  
        return instance;  
    }  
}

優點:

  • 在內存裏只有一個實例,可以節省內存空間,減輕GC壓力;
  • 避免頻繁的創建和銷燬對象,可以提高性能;
  • 避免對資源的多重佔用、同時操作。

實現方式

餓漢

上面的示例即是餓漢式。
餓漢式是線程安全的,基於 classloader 機制避免了多線程的同步問題,不用加鎖,執行效率高,實現起來比較簡單。但是由於在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。

懶漢

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
	    if (instance == null) {  
	        instance = new Singleton();  
	    }  
    	return instance;  
    }  
}

懶漢模式雖然可以延遲加載,但是在多線程情況下並不能保證線程安全

加鎖

針對線程不安全的懶漢式的單例,可以通過給創建對象的方法加鎖來解決。每個線程在進入getInstance()方法前,要先等候別的線程離開該方法,即不會有兩個線程可以同時進入此方法執行 new Singleton(),從而保證了單例的有效。

public static synchronized Singleton getInstance() {
	if (instance == null) {
		instance = new Singleton();
	}
	return instance;
}

雙重檢查鎖

上一種方式因爲synchronized加鎖範圍是整個方法,該方法的所有操作都是同步進行的。但事實上,只有在第一次創建對象的時候需要加鎖,之後沒有必要進入if語句,也根本不需要同步操作,可以直接返回instance,在方法上加鎖反倒會造成性能下降。因此考慮將鎖由方法改爲代碼塊上,如下:

public static Singleton getInstance() {
	if (instance == null) {   
	    synchronized(Singleton.class){
	        instance = new Singleton();
	    }
	}
	return instance;
}

但是同步代碼塊並不能做到線程安全,仍可能產生多個實例,需要增加一個單例不爲空的判斷來確保線程安全,即雙重檢查方式。

public static Singleton getInstance() {
	if (instance == null) {   
	    synchronized(Singleton.class){
	        if (instance == null) {
	            instance = new Singleton();
	        }
	    }
	}
	return instance;
}

然而,雙重檢查鎖仍然存在隱患。
首先先明確下instance = new Singleton();這句話是怎麼執行的,new一個對象需要如下幾個步驟:

  1. 判斷class對象是否加載,如果沒有就先加載class對象;
  2. 分配內存空間;
  3. 調用構造函數,初始化實例;
  4. 返回地址給引用。

而cpu爲了優化程序,可能會進行指令重排序,打亂這3,4這兩個步驟。

舉個栗子,instance剛被分配了內存空間,還沒完成new Singleton()的全部操作,其他線程在進行第一次判斷instance是否爲空的的時候,結果是false(由於instance指向的是個內存地址,所以分配了內存空間之後,instance這個變量已經不爲空),這個時候這個線程就會直接返回,然而instance變量指向的內存空間還沒完成new Singleton()的全部操作。 這樣一來,一個沒有被完全初始化的instance就會被使用。

而volatile可以禁止指令重排序,保證多個線程可以正確獲取單例實例,改動如下:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton(){}  
    public static Singleton getSingleton() {  
	    if (singleton == null) {  
	        synchronized (Singleton.class) {  
		        if (singleton == null) {  
		            singleton = new Singleton();  
		        }  
	        }  
	    }  
	    return singleton;  
    }  
}

靜態代碼塊

靜態代碼塊方式事實上是餓漢式的一種變種,由於當類被加載時,靜態代碼塊也會被執行,因此並沒有真的解決懶加載的問題。

public class Singleton { 
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton(){
    }
    public static Singleton getInstance(){  
        return instance;  
    }  
} 

靜態內部類

靜態內部類同樣利用了classloder的機制來保證初始化instance時只有一個線程,跟餓漢式不同之處在於餓漢式只要Singleton類被裝載,instance就會被實例化,而靜態內部類是Singleton類被裝載了,instance並不一定被初始化。
因爲Inner 類沒有被主動使用,只有顯示通過調用getInstance方法時,纔會顯示裝載Inner類,從而實例化instance。如果考慮延遲加載,可以考慮此方式。

public class Singleton { 
    private Singleton(){
    }
    public static Singleton getInstance(){  
        return Inner.instance;  
    }  
    private static class Inner {  
        private static final Singleton instance = new Singleton();  
    }  
} 

單例&序列化

序列化可能會破壞單例模式,它會通過反射調用無參數的構造方法創建一個新的實例,通過對Singleton的序列化與反序列化得到的對象是一個新的實例。
在反序列化操作時,從流中讀取對象的 readObject() 方法之後會創建並返回一個新的實例。如果在 readResolve() 方法中用原來的 instance 替換掉從流中讀取到的新創建的 instance,就可以避免使用序列化方式破壞單例了。

// You can prevent this by using readResolve() method, since during serialization readObject() is used to create instance and it return new instance every time but by using readResolve you can replace it with original Singleton instance.  
private Object readResolve() {
   return instance;
}

枚舉

爲了解決序列化的問題,還可以通過枚舉的方式來解決。
Java規範中規定,每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的,因此在枚舉類型的序列化和反序列化上,Java做了特殊的規定。在序列化時Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf()方法來根據名字查找枚舉對象,從而保證了枚舉實例的唯一性。

public enum Singleton {
    INSTANCE;//枚舉變量
    private VariableName variableName;//枚舉變量屬性
    Singleton(){
        //此處完成屬性的賦值
        variableName=.....
    }
    //返回該屬性
    public  VariableName getVariableName(){
        return variableName;
    }
}

單例&克隆

通常來講,單例模式的類是不可以實現 Cloneable 接口的。
但是如果實現了,如何防止通過clone()來創建一個創建單例實例的另一個實例呢?
可以 override 它的 clone() 方法,使其拋出異常來防止。

Preferred way is not to implement Clonnable interface as why should one wants to create clone() of Singleton and if you do just throw Exception from clone() method as  “Can not create clone of Singleton class“。
@Override
public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

ps:枚舉不支持克隆,因爲Enum類已經將clone()方法定義爲final了。

單例&反射

如何防止通過反射來創建一個創建單例實例的另一個實例呢?
如果藉助AccessibleObject.setAccessible方法,通過反射機制調用私有構造器創建對象。
要防止此情況發生,可以在私有的構造器中加一個判斷,需要創建的對象不存在就創建;存在則拋出異常提示單例已經初始化。

Since constructor of Singleton class is supposed to be private it prevents creating instance of Singleton from outside but Reflection can access private fields and methods, which opens a threat of another instance. This can be avoided by throwing Exception from constructor as “Singleton already initialized”
private Singleton(){
    if(null!=instance){
        throw new RuntimeException("Singleton already initialized");
    }
}

其它

JDK中的單例

java.lang.Runtime

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     #getRuntime()
 * @since   JDK1.0
 */
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

Calendar

注意:
Calendar不是單例模式
單例模式要求每次返回的永遠是同一個對象,即對象的引用是相同的,而Calendar.getInstance()方法得到的對象是不同的對象。

/**
 * Gets a calendar using the default time zone and locale. The
 * <code>Calendar</code> returned is based on the current time
 * in the default time zone with the default
 * {@link Locale.Category#FORMAT FORMAT} locale.
 *
 * @return a Calendar.
 */
public static Calendar getInstance()
{
    return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}

private static Calendar createCalendar(TimeZone zone,
                                       Locale aLocale)
{
    CalendarProvider provider =
        LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
                             .getCalendarProvider();
    if (provider != null) {
        try {
            return provider.getInstance(zone, aLocale);
        } catch (IllegalArgumentException iae) {
            // fall back to the default instantiation
        }
    }

    Calendar cal = null;

    if (aLocale.hasExtensions()) {
        String caltype = aLocale.getUnicodeLocaleType("ca");
        if (caltype != null) {
            switch (caltype) {
            case "buddhist":
            cal = new BuddhistCalendar(zone, aLocale);
                break;
            case "japanese":
                cal = new JapaneseImperialCalendar(zone, aLocale);
                break;
            case "gregory":
                cal = new GregorianCalendar(zone, aLocale);
                break;
            }
        }
    }
    if (cal == null) {
        // If no known calendar type is explicitly specified,
        // perform the traditional way to create a Calendar:
        // create a BuddhistCalendar for th_TH locale,
        // a JapaneseImperialCalendar for ja_JP_JP locale, or
        // a GregorianCalendar for any other locales.
        // NOTE: The language, country and variant strings are interned.
        if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
            cal = new BuddhistCalendar(zone, aLocale);
        } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
                   && aLocale.getCountry() == "JP") {
            cal = new JapaneseImperialCalendar(zone, aLocale);
        } else {
            cal = new GregorianCalendar(zone, aLocale);
        }
    }
    return cal;
}

單例&垃圾回收

單例對象是否會被jvm回收

在hotspot虛擬機中,垃圾收集算法使用根搜索算法,基本思路就是通過一系列的稱爲“GC Roots”的對象作爲起始點, 從這些節點開始向下搜索, 搜索所走過的路徑稱爲引用鏈( Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連( 用圖論的話來 說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。

可以作爲根的對象有:

  • Local variables 虛擬機棧棧楨中的本地變量表;
  • Static variables 靜態屬性變量;
  • Active JAVA Threads 活着的線程;
  • JNI References JNI的引用的對象

由於java中單例模式創建的對象被自己類中的靜態屬性所引用,單例對象不會被jvm垃圾收集。

單例類是否會被JVM卸載

jvm卸載類的判定條件如下:

  • 該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

只有當以上三個條件都滿足,jvm纔會在垃圾收集的時候卸載類。由於單例實例不會被回收,單例類也就不會被卸載。也就是說,只要單例類中的靜態引用指向jvm堆中的單例對象,那麼單例類和單例對象都不會被垃圾收集,依據根搜索算法,對象是否會被垃圾收集與未被使用時間長短無關,僅僅在於這個對象是不是“活”的。

單例&spring

singleton && prototype

在spring中scope的默認值爲singleton,即單例。表明容器中創建時只存在一個實例,所有引用此bean都是單一實例。
此外,singleton類型的bean定義從容器啓動到第一次被請求而實例化開始,只要容器不銷燬或退出,該類型的bean的單一實例就會一直存活。
spring的單例默認爲餓漢模式,如果將default-lazy-init屬性或 lazy-init設置爲true,爲懶漢模式,在第一個請求時才初始化一個實例,以後的請求都調用這個實例。
Spring 對 Bean 實例的創建是採用單例註冊表的方式進行實現的,而這個註冊表的緩存是 ConcurrentHashMap 對象。單例註冊表的實現可以用於操作一組對象的聚集。
注意,lazy-init的優先級比default-lazy-init要高。

當scope配置爲prototype時,則每次都要新創建一個Bean實例。當Spring創建了bean的實例後,bean的實例就交給了客戶端的代碼管理,Spring容器將不再跟蹤其生命週期,並且不會管理那些被配置成prototype作用域的bean的生命週期,包括該對象的銷燬。

< beans  default-lazy-init ="true" >
   <bean id="service" type="bean路徑" lazy-init="true"/> 
</beans>

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * @param name the name of the bean to retrieve
 * @param requiredType the required type of the bean to retrieve
 * @param args arguments to use when creating a bean instance using explicit arguments
 * (only applied when creating a new instance as opposed to retrieving an existing one)
 * @param typeCheckOnly whether the instance is obtained for a type check,
 * not for actual use
 * @return an instance of the bean
 * @throws BeansException if the bean could not be created
 */
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
		final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
		throws BeansException {
     // 對 Bean 的 name 進行特殊處理,防止非法字符
	final String beanName = transformedBeanName(name);
	Object bean;

	// Eagerly check singleton cache for manually registered singletons.
	Object sharedInstance = getSingleton(beanName);
	if (sharedInstance != null && args == null) {
		...
		bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
	}

	else {
		// Fail if we're already creating this bean instance:
		// We're assumably within a circular reference.
		...

		try {
			final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
			checkMergedBeanDefinition(mbd, beanName, args);

			// Guarantee initialization of beans that the current bean depends on.
			String[] dependsOn = mbd.getDependsOn();
			if (dependsOn != null) {
				for (String dep : dependsOn) {
					if (isDependent(beanName, dep)) {
						throw new BeanCreationException(mbd.getResourceDescription(), beanName,
								"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
					}
					registerDependentBean(dep, beanName);
					getBean(dep);
				}
			}

			// Create bean instance.
			if (mbd.isSingleton()) {
				sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
					@Override
					public Object getObject() throws BeansException {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							// Explicitly remove instance from singleton cache: It might have been put there
							// eagerly by the creation process, to allow for circular reference resolution.
							// Also remove any beans that received a temporary reference to the bean.
							destroySingleton(beanName);
							throw ex;
						}
					}
				});
				bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
			} else if (mbd.isPrototype()) {
				// It's a prototype -> create a new instance.
				Object prototypeInstance = null;
				try {
					beforePrototypeCreation(beanName);
					prototypeInstance = createBean(beanName, mbd, args);
				}
				finally {
					afterPrototypeCreation(beanName);
				}
				bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
			} else {
				...
			}
		}
		catch (BeansException ex) {
			...
		}
	}

	// Check if required type matches the type of the actual bean instance.
	...
	return (T) bean;
} 

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/**
 * Return the (raw) singleton object registered under the given name.
 * <p>Checks already instantiated singletons and also allows for an early
 * reference to a currently created singleton (resolving a circular reference).
 * @param beanName the name of the bean to look for
 * @param allowEarlyReference whether early references should be created or not
 * @return the registered singleton object, or {@code null} if none found
 */
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	Object singletonObject = this.singletonObjects.get(beanName);
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		synchronized (this.singletonObjects) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
				ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
				if (singletonFactory != null) {
					singletonObject = singletonFactory.getObject();
					this.earlySingletonObjects.put(beanName, singletonObject);
					this.singletonFactories.remove(beanName);
				}
			}
		}
	}
	return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

有狀態&&無狀態

有狀態會話bean :每個用戶有自己特有的一個實例,在用戶的生存期內,bean保持了用戶的信息,即“有狀態”;一旦用戶滅亡(調用結束或實例結束),bean的生命期也告結束。即每個用戶最初都會得到一個初始的bean。簡單來說,有狀態就是有數據存儲功能。有狀態對象(Stateful Bean),就是有實例變量的對象 ,可以保存數據,是非線程安全的。
無狀態會話bean :bean一旦實例化就被加進會話池中,各個用戶都可以共用。即使用戶已經消亡,bean 的生命期也不一定結束,它可能依然存在於會話池中,供其他用戶調用。由於沒有特定的用戶,那麼也就不能保持某一用戶的狀態,所以叫無狀態bean。但無狀態會話bean 並非沒有狀態,如果它有自己的屬性(變量),那麼這些變量就會受到所有調用它的用戶的影響,這是在實際應用中必須注意的。簡單來說,無狀態就是一次操作,不能保存數據。無狀態對象(Stateless Bean),就是沒有實例變量的對象 .不能保存數據,是不變類,是線程安全的。

有狀態bean的線程安全

  • 將有狀態的bean配置成prototype模式,讓每一個線程都創建一個prototype實例。
  • 使用ThreadLocal變量,爲每一條線程設置變量副本。

對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,從而隔離了多個線程對數據的訪問衝突,因此可以同時訪問而互不影響。

spring對事務的處理即是通過threadLocal來保證線程安全的。

public abstract class TransactionSynchronizationManager {

	private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<Map<Object, Object>>("Transactional resources");

	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations");

	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<String>("Current transaction name");

	private static final ThreadLocal<Boolean> currentTransactionReadOnly =
			new NamedThreadLocal<Boolean>("Current transaction read-only status");

	private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
			new NamedThreadLocal<Integer>("Current transaction isolation level");

	private static final ThreadLocal<Boolean> actualTransactionActive =
			new NamedThreadLocal<Boolean>("Actual transaction active");
}

/**
 * Bind the given resource for the given key to the current thread.
 * @param key the key to bind the value to (usually the resource factory)
 * @param value the value to bind (usually the active resource object)
 * @throws IllegalStateException if there is already a value bound to the thread
 * @see ResourceTransactionManager#getResourceFactory()
 */
public static void bindResource(Object key, Object value) throws IllegalStateException {
	Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
	Assert.notNull(value, "Value must not be null");
	Map<Object, Object> map = resources.get();
	// set ThreadLocal Map if none found
	if (map == null) {
		map = new HashMap<Object, Object>();
		resources.set(map);
	}
	Object oldValue = map.put(actualKey, value);
	// Transparently suppress a ResourceHolder that was marked as void...
	if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) {
		oldValue = null;
	}
	if (oldValue != null) {
		throw new IllegalStateException("Already value [" + oldValue + "] for key [" +
				actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
	}
	if (logger.isTraceEnabled()) {
		logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" +
				Thread.currentThread().getName() + "]");
	}
}
發佈了35 篇原創文章 · 獲贊 5 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章