單例模式詳解
1.1單例模式概述
單例模式(Singleton Pattern)指確保一個類在任何情況下都絕對只有一個實體,并提供一個全域訪問點,屬于創建型設計模式,
1.2單例模式的應用場景
單例模式可以保證JVM中只存在單一實體,應用場景主要有以下幾個方面:
- 需要頻繁創建一些類的物件,使用單例模式可以降低系統的記憶體壓力,減少GC,
- 一些類創建實體的程序復雜且占用資源過多,或耗時較長,并且經常使用,
- 系統上需要單一控制邏輯的操作,
1.3單例模式的寫法分類
1.3.1“餓漢式”單例寫法
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return instance;
}
}
上邊是餓漢式單例的標準寫法,可以看到餓漢式這種方式會在類加載的時候立刻初始化,并且創建單例物件,它是絕對的執行緒安全,因為在執行緒還沒有訪問的時候已經實體化結束,不可能存在訪問安全的問題,
- 餓漢式的另一種寫法
public class HungrySingleton {
private static final HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
}
這種寫法采用靜態代碼塊的機制,
餓漢式單例優缺點分析
餓漢式單例的寫法適用于單例物件較少的情況,這樣寫可以保證絕對的執行緒安全,執行效率比較高,但是缺點也很明顯,餓漢式會在類加載的時候就將所有單例物件實體化,這樣系統中如果有大量的餓漢式單例物件的存在,系統初始化的時候會造成大量的記憶體浪費,從而導致系統的記憶體不可控,換句話說就是不管物件用不用,物件都已存在,占用記憶體,
為了解決餓漢式寫法帶來記憶體浪費的問題,引出了懶漢式寫法,
1.3.2懶漢式單例寫法
- 特點:單例物件在被使用的時候才會進行實體化
public class LazySingleton {
private LazySingleton(){}
private static LazySingleton instance;
public static LazySingleton getInstance(){
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
上邊是懶漢式單例的標準寫法,從代碼可以看出單例物件只有在被使用的時候才會進行實體化,但是這種方式又會引入一個新的問題,在多執行緒的環境下執行,存在執行緒安全問題,可能破壞單例,
驗證一下:
public class ExcutorThread implements Runnable{
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+":"+instance);
}
}
public class LazySingletonTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new ExcutorThread());
Thread thread2 = new Thread(new ExcutorThread());
thread1.start();
thread2.start();
System.out.println("end");
}
}
由于我們這里只有兩條執行緒模擬多執行緒執行,需進行多次才能獲取破壞單例情況,以下為捕捉到的單例被破壞情況:

那么我們如何解決執行緒不安全呢?第一個應該想到的就是synchronized關鍵字:
public class LazySingleton {
private LazySingleton(){}
private static LazySingleton instance;
public static synchronized LazySingleton getInstance(){
if (instance == null){
instance = new LazySingleton();
}
return instance;
}
}
這樣我們就解決了執行緒安全問題,雖然目前解決了執行緒安全問題,但是當呼叫getInstance方法的執行緒很多,只有一個執行緒RUNNING,其他執行緒會出現阻塞,又引入了性能問題,我們來進一步優化,
- 雙重檢鎖方式(Double Check)
public class LazySingleton {
private LazySingleton(){}
private volatile static LazySingleton instance;
public static LazySingleton getInstance(){
// 是否會進行阻塞
if (instance == null){
synchronized (LazySingleton.class){
// 是否需要創建實體
if (instance == null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
減少鎖的碰撞幾率,并且將鎖放置到getInstance內部,呼叫者感受不明顯,
但是只要有鎖就會對性能有一定影響,這時我們從類初始化的角度考慮,引出靜態內部類的方式,
- 靜態內部類寫法
public class LazySingletonInnerClass {
private LazySingletonInnerClass(){}
public static LazySingletonInnerClass getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
}
}
只有在呼叫getInstance將內部類加載,實作懶加載,內部類只加載一次,執行緒安全,
思考:上邊這種方式就真的完美了嗎?我們所有的單例模式中構造方法私有化,我們一直在獲取實體的方法下功夫,而JAVA提供的反射是完全可以破壞私有化構造方法的,接下來嘗試用反射破壞:
public static void main(String[] args) {
try{
Class<?> clazz = LazySingletonInnerClass.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
// 開啟強制訪問
declaredConstructor.setAccessible(true);
// 獲取兩個實體 破壞單例
Object instance1 = declaredConstructor.newInstance();
Object instance2 = declaredConstructor.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
這個時候構造方法被呼叫,破壞了單例,那我們徹底不讓呼叫構造方法,呼叫構造方法時候進行拋例外,繼續優化,
public class LazySingletonInnerClass {
private LazySingletonInnerClass(){
throw new RuntimeException("不允許呼叫構造方法");
}
public static LazySingletonInnerClass getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
}
}
但是在構造方法中拋出例外,著實不妥,但是JAVA又又天然不允許反射呼叫構造方法的類---列舉,引出列舉單例模式
- 列舉單例模式
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
驗證一下:
public static void main(String[] args) {
try{
Class<?> clazz = EnumSingleton.class;
Constructor<?> c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton instance =(EnumSingleton) c.newInstance("123", 12);
System.out.println(instance);
}catch (Exception e){
e.printStackTrace();
}
}
運行結果:

真相大白,列舉不可用反射破壞構造方法,JDK的處理方式是最權威的,JDK列舉的特殊性,讓代碼實作更優雅,
至此,我們已經分析了各種創建單例物件時候出現的問題以及解決辦法,但是創建完單例物件之后,有時候我們會使用序列化將物件寫入磁盤,當下次使用時再從磁盤中反序列化轉化為記憶體物件,這樣也會破壞單例模式,
驗證一下:
public static void main(String[] args) {
HungrySingleton s1 = null;
HungrySingleton s2 = HungrySingleton.getInstance();
try{
// 序列化
FileOutputStream fileOutputStream = new FileOutputStream("s2.obj");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(s2);
objectOutputStream.flush();
objectOutputStream.close();
// 反序列化
FileInputStream fileInputStream = new FileInputStream("s2.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
s1 = (HungrySingleton) objectInputStream.readObject();
objectInputStream.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}catch (Exception e){
e.printStackTrace();
}
}
結果破壞了單例,見下圖:

那么如何保證在序列化的情況下保證單例呢?很簡單,只需要增加readResolve方法,
public class HungrySingleton implements Serializable {
private static final HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
private Object readResolve(){
return instance;
}
}

這個原因的分析,因為在反序列話程序中創建出了個新物件,所以需要看下readObject原始碼中是如何實作的,
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, 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();
}
}
}
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
......
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared)); // 關鍵方法標記
......
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;
}
/**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);
}
上邊這個判斷的意思是如果這個類是實作了serializable/externalizable,并且可以由序列化運行時實體化,則回傳true,這個時候obj = desc.newInstance 就會創建一個新的物件,所以這個時候單例就被破壞了,
此時我們在往下邊看,desc.hasReadResolveMethod()這個方法:
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
根據注釋說的意思是如果我們加上這個readResolve()方法,判斷結果就是true,會進入if塊,在看if中Object rep = desc.invokeReadResolve(obj);方法:
/**
* 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();
}
}
這個方法可以看出,利用反射去執行我們類中的readResolve()方法,這塊readResolveMethod的賦值,可以再ObjectStreamClass類中:
private ObjectStreamClass(final Class<?> cl) {
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl);
Class<?> superCl = cl.getSuperclass();
superDesc = (superCl != null) ? lookup(superCl, false) : null;
localDesc = this;
if (serializable) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (isEnum) {
suid = Long.valueOf(0);
fields = NO_FIELDS;
return null;
}
if (cl.isArray()) {
fields = NO_FIELDS;
return null;
}
suid = getDeclaredSUID(cl);
try {
fields = getSerialFields(cl);
computeFieldOffsets();
} catch (InvalidClassException e) {
serializeEx = deserializeEx =
new ExceptionInfo(e.classname, e.getMessage());
fields = NO_FIELDS;
}
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = https://www.cnblogs.com/amazing-yml/archive/2022/03/03/(writeObjectMethod != null);
}
domains = getProtectionDomains(cons, cl);
writeReplaceMethod = getInheritableMethod(
cl,"writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class); //關鍵方法標記
return null;
}
});
} else {
suid = Long.valueOf(0);
fields = NO_FIELDS;
}
try {
fieldRefl = getReflector(fields, this);
} catch (InvalidClassException ex) {
// field mismatches impossible when matching local fields vs. self
throw new InternalError(ex);
}
if (deserializeEx == null) {
if (isEnum) {
deserializeEx = new ExceptionInfo(name, "enum type");
} else if (cons == null) {
deserializeEx = new ExceptionInfo(name, "no valid constructor");
}
}
for (int i = 0; i < fields.length; i++) {
if (fields[i].getField() == null) {
defaultSerializeEx = new ExceptionInfo(
name, "unmatched serializable field(s) declared");
}
}
initialized = true;
}
這個方法已經約定方法名稱readResolve,這時候執行類中的readResolve方法,直接回傳已經創建的實體,所以反序列化后的結果變成了在單例類中已創建的實體物件,
1.4 單例模式的擴展
1.4.1 ioc容器的啟蒙
- 容器式單例的寫法
/**
* Created by yml
*/
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className){
if (!ioc.containsKey(className)){
Object obj = null;
try{
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
}catch (Exception e){
e.printStackTrace();
}
return obj;
}else {
return ioc.get(className);
}
}
}
這是容器式單例的寫法,適用于需要大量創建單例物件的場景,便于管理,但是它是非執行緒安全的,其實spring中存盤單例物件的容器就是一個Map,
1.4.2 ThreadLocal單例詳解
- ThreadLocal單例寫法
package org.example.Singleton;
/**
* Created by yml
*/
public class ThreadLocalSingleton {
private ThreadLocalSingleton(){}
private static final ThreadLocal<ThreadLocalSingleton> threadInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadInstance.get();
}
}
ThreadLocal的單例實作不能保證創建的單例全域唯一,但是可以保證單個執行緒內部是唯一的,所以是執行緒安全的,之前保證單例執行緒安全的方法是加鎖,這種是以時間換空間的方式,將其他執行緒加鎖,ThreadLocal是將物件保存到ThreadLocalMap中,以空間換時間保證執行緒安全,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/436955.html
標籤:其他
