單例模式
- 1. 單例模式
- 2. 餓漢式單例
- 3. 懶漢式單例
- 3.1 方法加鎖寫法
- 3.2 代碼塊加鎖寫法
- 3.3 雙重判斷加鎖寫法
- 3.4 靜態內部類寫法
- 4. 注冊式單例
- 4.1. 列舉寫法注冊式單例
- 4.2. Spring IOC容器注冊式單例
- 5. ThreadLocal單例
- 6. 反射破壞單例證明
- 7. 高高高手需要知道的-序列化破壞單例
日頭沒有辜負我們,我們也切莫辜負日頭,——沈從文
代碼世界中也存在以下順口溜:
我單身,我驕傲,我為國家省套套,
我單身,我自豪,我為祖國省橡膠,
單例模式雖然簡單,但真正懂的內行的人并不多,今天挑戰全網最全的經典設計模式之單例模式,
1. 單例模式
定義
確保一個類在任何情況下都絕對只有一個實體,并提供一個全域訪問點,
隱藏其構造方法
屬于創建型設計模式
適用場景
確保任何情況下都絕對只有一個實體
ServletContext、ServletConfig、ApplicationContext、DBPool
2. 餓漢式單例
定義
系統初始化的時候就加載,不管有沒有用到這個單例,
優點
執行效率高,性能高,沒有任何的鎖
缺點
某些情況下,可能會造成記憶體浪費
能夠被反射破壞
代碼
public class HungrySingleton {
private static final HungrySingleton singleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance() {
return singleton;
}
}
3. 懶漢式單例
定義
系統初始化的時候不創建實體,只有用到的時候才創建實體,
優點
節省了記憶體
缺點
synchronized造成性能低下
能夠被反射破壞
3.1 方法加鎖寫法
代碼
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本1
* @return
*/
private synchronized LazySingleton getInstance() {
if (null == singleton) {
singleton = new LazySingleton();
}
return singleton;
}
}
3.2 代碼塊加鎖寫法
代碼
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本2 相比版本1優化一點點
* @return
*/
private LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
return singleton;
}
}
3.3 雙重判斷加鎖寫法
陷阱案例
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本3 雙重判斷
* @return
*/
private LazySingleton getInstance() {
if (null == singleton) {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
版本3看起來相比版本2優化了不少,但其實這種雙重判斷在生產環境有一個極大的漏洞陷阱,就是指令重排序,有需要了解的可以在評論區留言,解決方案也很簡單,就是 volatile 關鍵字,它可以限制指令重排序,
正確寫法
public class LazySingleton {
private volatile static LazySingleton singleton = null;
private LazySingleton(){}
/**
* 版本3 雙重判斷
* @return
*/
private LazySingleton getInstance() {
if (null == singleton) {
synchronized (LazySingleton.class) {
if (null == singleton) {
singleton = new LazySingleton();
}
}
}
return singleton;
}
}
雙重判斷的優點:性能高了,執行緒安全了,
缺點:代碼可讀性極差,不夠優雅,
3.4 靜態內部類寫法
利用JVM加載類的順序,靜態內部類,只有用到的時候外部類用到靜態內部類的時候才會加載,
優點
寫法優雅,利用了Java的語法特點,性能高,避免了記憶體浪費
缺點
能夠被反射破壞
public class LazyStaticInnerSingleton {
private LazyStaticInnerSingleton(){}
public static LazyStaticInnerSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
}
}
這種寫法本來應該夠優雅,夠完美,但是卻有一個缺點是能被反射破壞,文章最后我會證明什么是能被反射破壞,那有沒有寫法能讓這個單例不會被反射破壞?答案是有的!
public class LazyStaticInnerSingleton {
private LazyStaticInnerSingleton(){
if (null != LazyHolder.INSTANCE) {
throw new RuntimeException("不允許非法訪問!");
}
}
public static LazyStaticInnerSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton();
}
}
這種寫法就解決了被反射破壞的問題,但是看起來不是那么的優雅,
4. 注冊式單例
定義
將每一個實體都快取到統一的容器中,使用唯一標識獲取實體,
4.1. 列舉寫法注冊式單例
優點
寫法優雅,執行緒安全
缺點
和餓漢式類似,大量使用會造成記憶體浪費,根本原因在于列舉本身的特點,
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;
}
}
使用方法
public class Test {
public static void main(String[] args) {
EnumSingleton singleton = EnumSingleton.getInstance();
singleton.setData(new Object());
singleton.getData();
}
}
4.2. Spring IOC容器注冊式單例
Spring設計者結合列舉式單例的寫法和特點,寫了一種自己的IOC 容器注冊式單例,
public class ContainerSingleton {
private ContainerSingleton() {}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public Object getInstance(String className) {
if (!ioc.containsKey(className)) {
Object instance = null;
try {
instance = Class.forName(className).newInstance();
} catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
5. ThreadLocal單例
ThreadLocal單例肯定會用到ThreadLocal,根據ThreadLocal本身的特點,即同一執行緒內資料可見,那么這種單例就有本身的局限性,使用的很少,我曾經在token登陸的時候用到過,即前端會傳一個token到后端,token能決議出登陸用戶的資訊,把決議后的資訊放在ThreadLocal中,那么本次處理請求就能在任何地方獲取登陸用戶資訊,
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
6. 反射破壞單例證明
public class Test1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = HungrySingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);//會輸出false
}
}
解決方案就是:構造方法拋例外,
if (null != LazyHolder.INSTANCE) {
throw new RuntimeException("不允許非法訪問!");
}
7. 高高高手需要知道的-序列化破壞單例
首先你必須知道什么是序列化,序列化就是JVM記憶體中的物件,序列化到磁盤檔案,再讀取到記憶體,不同行程的資料互動需要序列化才能傳輸,
以上的所有單例模式,解決了各種各樣的問題,但都存在同一個問題,就是都會被序列化破壞,意思就是:系統中的單例,被序列化到磁盤,然后再加載到記憶體,那么這序列化前后兩個單例,并不是同一個單例,這就是序列化破壞單例,
解決方案:在單例中加入以下方法:
private Object readResolve() {
// instead of the object we're on,
// return the class variable INSTANCE
return INSTANCE;
}
鑒于評論區有問為什么加入readResolve()方法就能解決序列化破壞單例的問題,現在我詳細講一下:
- 首先要知道,序列化一個物件怎么寫?(JAVA架構師之路四我會詳細講到)
public User deapClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (User)ois.readObject();
}
deapClone()方法就是序列化的程序,讀記憶體,寫記憶體,看最后ois.readObject(),我們來看看readObject()方法干了啥?
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();
}
}
}
方法里面有個Object obj = readObject0(false); 的readObject0()方法,我們再來看看這個方法做了啥?
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
方法太長 ,我粘貼主要的放啊case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); 如果是轉化成物件,要執行有個checkResolve 方法,這里是不是和readResolve() 優點關系了?都有resolve了,再看看readOrdinaryObject(unshared) 做了啥:
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;
desc.hasReadResolveMethod())檢測了有沒有readResolve() 方法,如果有,利用反射呼叫這個方法,這個方法回傳值是單例這個物件,最后或者物件賦值給obj,回傳,重寫完成,看到這兒應該都懂了加入readResolve() 方法就能解決這個序列化破壞單例這個問題了吧!
感謝您閱讀本文,如果您覺得文章寫的對您有用的話,請您點擊上面的“關注”,點個贊,這樣您就可以持續收到《JAVA架構師之路》的最新文章了,文章內容屬于自己的一點點心得,難免有不對的地方,歡迎在下方評論區探討,你們的關注是我創作優質文章的動力,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/224793.html
標籤:java
上一篇:一道有關陣列正賦值排序的面試題
