衆所周知,單例模式分爲餓漢式和懶漢式,昨天在看了《spring5核心原理與30個類手寫實戰》以後才知道餓漢式有不少種寫法,分別適用於不一樣場景,避免反射,線程不安全問題。下面就各類場景、採用的方式及其優缺點介紹。java
1.第一種寫法 ( 定義即初始化)spring
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}複製代碼
public class Singleton{
private static final Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}複製代碼
餓漢式基本上就這兩種寫法。在spring框架中IoC的ApplicantsContext
就是使用的餓漢式單例,保證了全局只有一個ApplicationContext
,在應用啓動後就能獲取實例,以便於進行接下來的操做.編程
因其在程序啓動後就已經初始化,也不須要任何鎖保證線程安全 ,因此執行效率高複製代碼
由於在程序啓動後就已經進行了初始化,即使是不用也進行了初始化,因此不管什麼時候都佔用內存空間,浪費了內存空間。
複製代碼
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}複製代碼
上面的代碼不難看出,在單線程下執行是沒有問題的,但在多線程狀況下,線程執行速度和順序沒法控制肯定,故有可能會產生多個實例對象,這樣就違背了單例模式的初衷了。安全
加鎖保證線程安全(synchronized
關鍵字)多線程
public class Singleton{
private static final Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if(instance == null){
instance = new Singleton();
}
return instance;
}
}複製代碼
能夠看到在getInstance()
上加了synchronized
關鍵字,就能保證線程同步。但又有一個問題:使用synchronized關鍵字是,當一個線程調用獲取實例的方法時,會鎖住整個類,其餘的線程再調用,會使線程狀態由 RUNNING 變成 MONITOR ,進而致使線程阻塞,執行效率降低;知道這個線程執行完實例方法,其餘線程才能繼續執行,兩個線程時,效率降低還在能夠接受範圍內,但在實際應用場景中,使用線程池來管理線程的調度,會有大量的線程,若是這些線程都阻塞了,其結果能夠預見。併發
上述問題有什麼更好的問題解決呢?使用雙重檢查鎖機制能夠完美的解決這個問題。其代碼以下複製代碼
public class Singleton{
private volatile static final Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null){
synchronized(Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}複製代碼
這裏須要解釋下,童鞋們都知道一個對象使用要經歷一下步驟:app
在java中JVM爲了提升執行效率,會進行指令重排。那什麼時指令重排呢?**指令重排**是指JVM爲了優化指令,提升程序的運行效率,在不影響單線程執行結果的狀況下,進行指令重排序,以期提升並行度。複製代碼
有上述能夠指令重排在單線程狀況下,對程序的執行不會產生影響,但在多線程狀況下就不必定了。因此上述過程的執行順序可能發生變化,進而致使程序並不會按照預想的執行。框架
爲解決上述問題以及保證併發編程的正確性,java中定義了 **happens-before**原則。在 《JSR-133:Java Memory Model and Thread Specification》 書中關於happens-before定義是這樣的:複製代碼
1.若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。ide
2.兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法。性能
在Java中 爲 避免指令重排出現,引入了volatile 關鍵字。正如你所看到那樣在實例對象前就能保證執行結果的正確性。當一個線程調用` getInstance()` 方法時,執行到synchronized關鍵字時就會上鎖,其餘線程也調用時就會發生阻塞,固然這種阻塞不是鎖住整個類,而是僅僅鎖住了方法。如過方法中的邏輯不是太複雜的話,對於外界來講是感知不到的。複製代碼
這種方法終歸仍是要加鎖的,只要加鎖就會對程序性能產生影響。有什麼解決辦法能夠實現不加鎖,又能保證線程安全呢?
內部類:是指 一個類定義在另外一個類裏面或者一個方法裏面 的類。有如下特色:
靜態內部類:顧名思義 就是在內部類上加個static關鍵字 ,其特色有:
靜態內部類在載入Java的時候默認不加載,只有調用時進行加載。根據此特色雙鎖檢查機制的單例模式能夠改進使用靜態內部類。
代碼示例
public class Singleton{
private Singleton() {}
public static Singleton getInstance() {
return SingletonIner.instance;
}
//static是爲了單例內存共享,保證這個方法不會被重寫,重載
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}複製代碼
上述方法及解決了餓漢式的內存浪費問題,又解決了懶漢式的鎖的性能問題。
你們都知道在Java的各個框架中由於要實現某種功能,不可避免的使用到反射。反射有破壞封裝性和性能低下的問題。在這裏不考慮性能,只考慮封裝性被破壞的問題。調用者使用反射,破壞了封裝性,進而使實例有可能不止一個,這樣就違背了使用單例模式的初衷。
如何解決呢?很簡單,就是在建立另外的對象拋出異常,警告調用者,使其按照咱們預想的方式進行調用。
public class Singleton{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不容許建立多個實例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
}複製代碼
上面代碼可使調用者按照咱們的想法使用。
在實際應用中,爲保存對象到磁盤或其餘的存儲介質,不可避免的要使用序列化。一個單例建立好以後,將其序列化保存在磁盤上,下次使用時在反序列化取出放到內存中使用。反序列化後的對象會從新分配內存,即從新建立,這樣就違反了單例模式的初衷。以使用靜態內部類的代碼爲咱們單例模式類,下面進行簡單測試。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Main{
public static void main(String[] args) {
Singleton s1=null;
Singleton s2 = Singleton.getInstance();
FileOutputStream fos = null;
try {
fos=new FileOutputStream("singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis =new FileInputStream("singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Singleton)ois.readObject();
ois.close();
System.out.println(s1==s2)
}
catch(Exception e){
e.printStackTrace();
}
}
}複製代碼
上面代碼運行後發現,輸出居然時false,這就說明反序列化後和序列話前的對象不是同一個,實例化了兩次,根本不符合單例模式的原則。
如何改進呢? 改進 的方法也很簡單就是增長readResolve()
方法就能夠。下面看代碼
import java.io.Serializable;
public class Singleton implements Serializable{
private Singleton() {
if(SingletonIner.instance!=null){
throw new RuntimeException("不容許建立多個實例");
}
}
public static Singleton getInstance() {
return SingletonIner.instance;
}
private static class SingletonIner{
private static Singleton instance = new Singleton();
}
private Object readResolve() {
return SingletonIner.instance;
}
}複製代碼
深究一下,爲何會這樣呢?下面咱們來看看ObjectInputStream
裏的readObject()
方法一探究竟。代碼以下:
/**
* Read an object from the ObjectInputStream. The class of the object, the
* signature of the class, and the values of the non-transient and
* non-static fields of the class and all of its supertypes are read.
* Default deserializing for a class can be overridden using the writeObject
* and readObject methods. Objects referenced by this object are read
* transitively so that a complete equivalent graph of objects is
* reconstructed by readObject.
*
* <p>The root object is completely restored when all of its fields and the
* objects it references are completely restored. At this point the object
* validation callbacks are executed in order based on their registered
* priorities. The callbacks are registered by objects (in the readObject
* special methods) as they are individually restored.
*
* <p>Exceptions are thrown for problems with the InputStream and for
* classes that should not be deserialized. All exceptions are fatal to
* the InputStream and leave it in an indeterminate state; it is up to the
* caller to ignore or recover the stream state.
*
* @throws ClassNotFoundException Class of a serialized object cannot be
* found.
* @throws InvalidClassException Something is wrong with a class used by
* serialization.
* @throws StreamCorruptedException Control information in the
* stream is inconsistent.
* @throws OptionalDataException Primitive data was found in the
* stream instead of objects.
* @throws IOException Any of the usual Input/Output related exceptions.
*/
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}複製代碼
根據註釋,咱們知道readObject()
方法讀取一個對象的類,類的簽名以及該類機器全部超類的非瞬時和非靜態的值。咱們看到在try後面又調用了重寫的readObject0()
方法,其代碼以下:
/**
* Underlying readObject implementation.
*/
private Object readObject0(boolean unshared) throws IOException {
.......
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
.......
}複製代碼
因篇幅的問題我省略了不重要的代碼。
由上面看到,在TC_OBJECT處又調用了`readOrdinaryObject()` 方法,其源碼以下:
/**
* Reads and returns "ordinary" (i.e., not a String, Class,
* ObjectStreamClass, array, or enum constant) object, or null if object's
* class is unresolvable (in which case a ClassNotFoundException will be
* associated with object's handle). Sets passHandle to object's assigned
* handle.
*/
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}複製代碼
由上述代碼可知,由調用了ObjectStreamClass
的isInstanctiable()
方法,方法體很是簡單,源碼以下 :
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}複製代碼
其做用就是構造方法是否爲空,構造方法不爲空就返回true。這意味着只要時無參構造方法就會實例化。
再回去看 readOrdinaryObject()
的源碼。先是判斷readResloveMethod
是否爲空,經過全局查找可知在私有方法ObjectStreamClass()
給其賦值,賦值代碼以下:
readResolveMethod = gerInheritableMethod(c1,"readResolve",null,Object.class);複製代碼
以後上述的邏輯找到一個 readResolve()
方法若是存在就調用 invokeReadResolve()
方法,其代碼以下:
/**
* Invokes the readResolve method of the represented serializable class and
* returns the result. Throws UnsupportedOperationException if this class
* descriptor is not associated with a class, or if the class is
* non-serializable or does not define readResolve.
*/
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}複製代碼
由invokeReadResource()
方法又使用反射調用 readResolveMethod()
,進而執行readResolve()
方法。
經過分析源碼能夠看出,readResolve()
方法雖然解決了單例模式被破壞的問題,可是其實例化兩次,只不過新建立的對象被覆蓋了而已 。若是建立的對象動做發生加快,就意味着內存開銷也隨之增大。這個問題如何解決呢?使用註冊式單例便可完美解決上訴問題。
public enum EnumSingleton{
INSTANCE;
private Object data;
/**
* @return Object return the data
*/
public Object getData() {
return data;
}
/**
* @param data the data to set
*/
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}複製代碼
通過反編譯分析源碼可知枚舉式單例是在靜態代碼塊中爲INSTANCE賦值,使餓漢式單例的體現。
那麼序列化和反序列化可否破壞嗎枚舉式單例呢? 答案是不能。同查看源碼可知枚舉類型是經過類名和對象名找到全局惟一的對象。因此,枚舉對象不可能加載屢次。
那麼反射呢?答案也是不能。在程序運行時會報java.lang.NoSuchMethodException
異常,其意思爲沒有找到無參的構造方法。查看java.lang.Enum
源碼可知枚舉類型只有一個protect
構造方法。通過測試,使用反射直接實例化枚舉對象時會出現 Cannot reflectively create objects
查看Constructor
的 newInstsnce()
方法可知,在方法體作了判斷,若是是枚舉類型則直接拋出異常。
看到這個詞,有的小夥伴的內心就想什麼是容器式單例。容器式單例就是在單例類中維護一個相似與Map的容器,這種方式在Spring中是很是常見的,衆所周知,Spring的Bean是全局單例的;Spring在內部維護着一個Map結構。在 org.springframework.beans.factory.support
包下 SimpleBeanDefinitionRegistry
爲咱們完美的解釋容器式單例,其源碼以下:
public class SimpleBeanDefinitionRegistry extends SimpleAliasRegistry implements BeanDefinitionRegistry {
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "'beanName' must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
this.beanDefinitionMap.put(beanName, beanDefinition);
}
@Override
public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
if (this.beanDefinitionMap.remove(beanName) == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
}
@Override
public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
BeanDefinition bd = this.beanDefinitionMap.get(beanName);
if (bd == null) {
throw new NoSuchBeanDefinitionException(beanName);
}
return bd;
}
@Override
public boolean containsBeanDefinition(String beanName) {
return this.beanDefinitionMap.containsKey(beanName);
}
@Override
public String[] getBeanDefinitionNames() {
return StringUtils.toStringArray(this.beanDefinitionMap.keySet());
}
@Override
public int getBeanDefinitionCount() {
return this.beanDefinitionMap.size();
}
@Override
public boolean isBeanNameInUse(String beanName) {
return isAlias(beanName) || containsBeanDefinition(beanName);
}
}複製代碼
其中BeanDefinition
是一個接口,儲存着各個單例對象的信息,由其實現類實現。對象名做爲Map的Key,BeanDefinition
做爲Map的值,維護着這個map 保證每一個對象全局單例.
由於Spring比較複雜,討論暫告一段落。下面會到咱們的主題,那咱們的 singleton
類如何實現容器式單例呢。下面看代碼:
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
public class Singleton {
private static Map <String,Object > ioc =new ConcurrentHashMap();
private Singleton() {}
public static Object getInstance(String name) {
synchronized(ioc) {
if (!ioc.containsKey(name)){
Object o=null;
try {
o=Class.forName(name).newInstance();
ioc.put(name, o);
}catch(Exception e) {
e.printStackTrace();
}
return o;
}
else {
return ioc.get(name);
}
}
}
}複製代碼
容器式單例適用於單例實例對象比較多的狀況下,方便管理。值得注意的是,他是線程不安全的。
註冊式單例就包括上面兩種形式,每一個都有不一樣的應用場景以及特色,要根據實際狀況靈活選擇。
下面我來介紹一種特殊的單例模式-----擁有 ThreadLocal
單例模式。
ThreadLocal
與單例模式話很少說,直接看代碼。
public class Singleton {
private static final ThreadLocal<Singleton> instance = new ThreadLocal<> (){
@Override
protected Singleton initialValue() {
return new Singleton();
}
};
private Singleton() {}
public static Object getInstance() {
return instance.get();
}
}複製代碼
爲何說他特殊呢?由於加了 ThreadLocal
關鍵字的單例類是線程內單例的,單線程共享不是單例的。你們能夠測試下,使用下面的測試代碼。
public class Main{
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Singleton.getInstance());
}
} ;
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
System.out.println("end");
}
}複製代碼
執行結果以下:
測試後發現主線程不管執行多少次,獲取的實例都是同一個,而兩個子線程卻得到了不一樣的實例。
本文章爲做者原創,其中參考了《spring5核心原理與30個類手寫實戰》以及互聯網上的內容。如要轉載請註明來源。若有錯誤,請評論或者私聊我,歡迎探討技術問題