單例模式
文章目錄
-
- 單例模式
-
- 前言
- 時間線
- 代辦
- 參考連結
- 定義
- 要點
- 優勢
- 關于結構的了解
-
- 角色
- 修飾符
- 總結
- 關于分類的了解
-
- 餓漢式與懶漢式的差別
- 懶漢式的實作
- 關于線程安全的了解
-
- 分析
- 解決
- synchronized修飾方法
- synchronized修飾代碼塊
- 雙重檢查
- 其它方式實作單例
-
- 枚舉實作單例
-
- 編碼
- 運作
- 分析
- 私有靜态内部類
-
- 私有靜态内部類的設計
- 組裝
- 分析
- 序列化安全
- 反射攻擊
-
- 攻擊與防禦
- 最終方案:枚舉
-
- 攻擊與防禦
- 總結
- 應用
-
- 身份證
-
- 解析
- 編碼
- 測試
- 結果
- 列印池
-
- 解析
- 編碼
- 測試
- 單例模式的影子
-
- JDK中的單例模式
-
- RunTime
- Spring Bean中的單例
- 問題
-
- 在多線程的條件下,單例模式的單例是如何保證的?
-
- 分析
- 總結
-
- 以上總結的單例模式有多少種?
- 幾個關鍵字構成單例模式
前言
單例模式,可以說是最基本但卻是最重要的設計模式之一。
但時間一長,總會忘記單例模式是什麼,有什麼。。。
是以這一篇,是複習的一篇了解和總結。
時間線
2020.09.18 - 2020.09.20 完成初稿
2020.09.24 完成反射攻擊相關内容
代辦
- 單例模式中的序列化安全
參考連結
Java-單例(廖雪峰)
關于一些原理講得非常不錯
結合JDK源碼看設計模式——單例模式
定義
單例模式確定某一個類隻有一個執行個體,而且自行執行個體化,并向整個系統提供這個執行個體,這個類稱為單例類,它提供全局通路的方法。
咬文嚼字
單例的單指的是單一的,即一個;例表示執行個體。
了解:
執行個體即new;自行執行個體化,即在類内執行個體化;
向整個系統提供這個執行個體:整個系統表示一個方法。提供的方式是通過類名擷取的方式。
要點
- 某個類隻有一個執行個體
- 必須自行建立這個執行個體
- 必須向整個系統提供這個執行個體
優勢
- 減少建立Java執行個體所帶來的開銷
- 便于系統跟蹤單個Java執行個體的生命周期,執行個體狀态等;
關于結構的了解
其實單例模式關鍵在于private和public兩個修飾符的使用,也可以說是2個private和1個public。
為了描述上面的了解。首先對單例模式結構的角色進行分析。
角色
- 構造方法
- 自身的靜态成員變量
- 靜态工廠方法
對于以下類Singleton,有以下内容
public class Singleton{
//自身的靜态成員變量
/*修飾符*/ static Singleton singleton;
//構造方法
private Singleton(){
}
//靜态工廠方法
/*修飾符*/ static Singleton getSingleton(){
return singleton;
}
}
修飾符
私有修飾符private可以實作類的方法或者變量不會被其它類使用的目的。
首先第1個私有修飾符,用于構造方法上,如上;
然後第2個私有修飾符,可以用在自身的靜态成員變量上,或者靜态工廠方法上。
最後的公有修飾符,則用在最後一個角色上。
還有一點——向整個系統提供這個執行個體的責任是誰?——其實誰都可以,也因為這樣,有多種分類。
就是所謂的餓漢式和懶漢式。見“關于分類的了解”
這裡姑且把這個責任讓自身的靜态成員變量承擔。
綜上,可以有2個分類,即:
- 自身靜态成員變量使用private
- 靜态工廠方法使用private
根據上面描述,則有以下的情況
- 自身的靜态成員變量使用私有修飾符
自身的靜态成員變量使用私有修飾符的話,
如果要實作向整個系統提供這個執行個體的話,就必須讓靜态工廠方法使用public修飾。
public class Singleton{
// 私有構造方法
private Singleton(){
}
// 私有自身的靜态成員變量
private static Singleton singleton = new Singleton();
// 公有靜态工廠方法
public static Singleton getInstance(){
return singleton;
}
}
- 靜态工廠方法使用私有修飾符
public class Singleton{
private Singleton(){
}
public static Singleton singleton = new Singleton();
//可有可無
private static Singleton getInstance(){
return singleton;
}
}
思考:這個私有的靜态工廠方法是否有存在的意義?
沒有的,
因為在類外,公有的自身靜态成員變量已經完成了向整個系統提供這個執行個體的責任
而且在類内,直接調用自身靜态成員變量也可以完成調用
總結
以上是關于單例模式結構的了解,關鍵在于2個private和1個public的配置設定
關于分類的了解
初學單例模式的時候,覺得結構非常簡單,隻有一個類,但是分類卻有很多,餓漢式,懶漢式或者枚舉等等,
又有線程安全和線程不安全的差別。
餓漢式與懶漢式的差別
餓和懶是兩個形容詞,一個表示吃,一個表示做。吃和做可以說是兩個不同的動作,也可以說是相同的。
餓的時候要吃東西,聯系動作“做”就有2種情況
- 食物已經做好了,一餓就吃
- 食物沒有做好,一餓就做。
回到單例模式,單例模式的差別就在于食物是否做好,做好了就不懶,是餓漢式;沒做好就是懶,是懶漢式;
懶不懶的差別在于是否在自身的靜态成員變量自身執行個體化,例如:
// 不懶,即餓漢式
/*修飾符*/ static Singleton singleton = new Singleton();
// 懶漢式
/*修飾符*/ static Singleton singleton;
回看 關于結構的了解,就會明白,關于結構的了解 子產品 所使用的就是相對簡單的餓漢式。
懶漢式的實作
懶漢式的實作的關鍵在于不在自身的靜态成員變量處執行個體化
另外,在結構上,懶漢式隻能将公有修飾符使用在靜态工廠方法上
原因:懶漢式有另一個名字lazy loading,即延遲加載
表示在調用方第一次調用
getSingleto()
時才初始化全局唯一執行個體
是以 靜态工廠方法承擔了向整個系統提供執行個體的責任。
如果将責任設定在靜态成員變量上,将無法執行個體。
public class Singleton{
private Singleton{
}
private static Singleton singleton = null;
public static Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
關于線程安全的了解
首先,線程安全問題是基于懶漢式而言的。利用最簡單的懶漢式模式進行分析,如下:
public class Singleton{
private Singleton{
}
private static Singleton singleton = null;
public static Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
分析
首先,該懶漢式是線程不安全的,隻能在單線程的情況下使用。
如果在多線程下,由于線程的執行速度是不确定的,容易出現競争條件,
一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行;
另一個線程也通過了這個判斷語句,這時便會産生多個執行個體(違背了單例模式的初衷)。
是以在多線程環境下不可使用這種方式。
解決
如果要讓懶漢式線程安全,就必須進行相應的改造。
當然有幾種方法,是以,懶漢式有線程不安全與線程安全的差別。
改造的關鍵在于同步鎖關鍵字synchronized的使用
synchronized修飾方法
使用synchronized修飾方法,簡單說一下synchronized的作用:
一個線程通路synchronized鎖定的範圍時,其他試圖通路的線程将被阻塞。
這裡的範圍指的是方法。
public class Singleton{
private Singleton{
}
private static Singleton singleton = null;
public synchronized static Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
分析
情景分析
當有一個線程通路該方法時,其它線程都會被阻塞。直至該線程完成通路。
效率分析
雖然在靜态工廠方法處添加同步鎖synchronized後,懶漢式是線程安全的。
但是這個靜态工廠方法隻有第一次通路時,運作
if()代碼塊裡
才需要進行同步,後面通路直接傳回執行個體就可以了。
就是說在方法處添加同步鎖synchronized将會影響效率。是以效率太低了。
通過以上分析,有個方案——在代碼塊處添加同步鎖。
synchronized修飾代碼塊
引用上面所講,
一個線程通路synchronized鎖定的範圍時,其他試圖通路的線程将被阻塞。
這裡的範圍指的是代碼塊
public class Singleton{
private Singleton{
}
private static Singleton singleton = null;
public static Singleton getSingleton(){
if(singleton == null){
sysnchronized(this){
singleton = new Singleton();
}
}
return singleton;
}
}
分析
情景分析
由于sysnchronized的作用範圍問題,是以同步鎖隻能作用于建立執行個體。
和最初的懶漢式結構是一樣的概念的。即一個線程來不及往下執行,另一個線程已經建立了執行個體,導緻産生多執行個體。
作用分析:無效
雙重檢查
如果上面兩個例子都無法實作在多線程情況下,使用懶漢模式。那麼雙重檢查就是在多線程的環境下使用懶漢模式的解決方案。
雙重檢查,Double-checked Locking,線程安全且高性能
public class Singleton{
private Singleton{
}
/*
1重鎖
Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值
避免指令重排
*/
private static volatile Singleton singleton = null;
public static Singleton getSingleton(){
// 2重鎖
// 雙重檢查
if(singleton == null){
sysnchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton(); // 不是原子性操作
}
// 配置設定記憶體空間
// 執行構造方法,構造對象
// 将對象指向記憶體空間
// 不使用 volative 可能 1 3 2
}
}
return singleton;
}
}
分析
其實雙重檢查是synchronized修飾代碼塊的一類,但是卻能夠實作線程安全。
其它方式實作單例
枚舉實作單例
JDK1.5中添加的枚舉來實作單例模式。因為Java保證枚舉類的每個枚舉都是單例。
不僅能避免多線程同步問題,而且還能防止反序列化重新建立新的對象。
編碼
public enum Singleton{
INSTANCE;
private Singleton getInstance{
return INSTANCE;
}
}
運作
Singleton singleton = Singleton.INSTANCE
分析
優點
系統記憶體中該類隻存在一個對象,節省了系統資源,對于一些需要頻繁建立銷毀的對象,使用單例模式可以提高系統性能。
使用枚舉實作Singleton還避免了第一種方式實作Singleton的一個潛在問題:即序列化和反序列化會繞過普通類的
private
構造方法進而建立出多個執行個體,而枚舉類就沒有這個問題。
缺點當想執行個體化一個單例類的時候,必須要記住使用相應的擷取對象的方法,而不是使用new,可能會給其他開發人員造成困擾,特别是看不到源碼的時候。
私有靜态内部類
之前有個結論——2個private和1個public,如果在類内建立一個私有靜态内部類,那麼這個私有靜态内部類就可以被類使用。
私有靜态内部類的設計
private static class SingletonInstance{
private static Singleton INSTANCE = new Singleton();
}
組裝
public class Singleton{
// 私有構造方法
private Singleton{
}
// 私有靜态内部類
private static class SingletonInstance{
private static Singleton INSTANCE = new Singleton();
}
// 公有靜态工廠方法
public static Singleton getSingleton(){
return SingletonInstance.INSTANCE;
}
}
分析
其實非常像餓漢式單例,微小差別在于将私有自身的靜态成員變量替換成私有靜态内部類。
這樣做,有什麼好處?
延遲加載:在Singleton被裝載的時候并不會立即執行個體化,隻有在需要的時候,即調用
的時候,才會裝載類
getSingleton()
SingletonInstance
,進而完成Singleton()的執行個體化。
線程安全
效率高
序列化安全
單例模式中的兩大類餓漢式和懶漢式都不是序列化。是以想要序列化安全,就必須做相應的修改。
安全的序列化與反序列化
// 防止 序列化
private Object readResolve(){
return INSTANCE;
}
待補充,學完序列化知識後進行補充
反射攻擊
以下将按 攻擊 -> 防禦 -> 攻擊 -> 防禦這樣順序編寫
攻擊與防禦
初始
public class Singleton {
private Singleton(){}
// 通過private static 變量持有唯一執行個體
private static final Singleton INSTANCE = new Singleton();
// 通過 public static 方法傳回唯一執行個體
public static Singleton getInstance(){
return INSTANCE;
}
}
攻擊:原始反射攻擊
// 反射攻擊
try {
Singleton01 singleton01 = Singleton01.getINSTANCE();
Constructor<Singleton01> constructor = Singleton01.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton01 singleton012 = constructor.newInstance();
System.out.println(singleton01 == singleton012); // 輸出結果 false // 反射攻擊成功
}catch (Exception e){
e.printStackTrace();
}
防禦:構造器加鎖
private Singleton(){
synchronized (Singleton01.class){
// 防止反射攻擊
if(INSTANCE != null){
throw new RuntimeException("防止反射攻擊");
}
}
}
// 輸出
/*
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at zhj.pro.reflectionattack.Main.main(Main.java:22)
Caused by: java.lang.RuntimeException: 防止反射攻擊
at zhj.pro.reflectionattack.Singleton.<init>(Singleton.java:18)
... 5 more
原始攻擊失敗!
*/
攻擊:使用反射建立對象
try {
Constructor<Singleton02> constructor = Singleton02.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton02 singleton022 = constructor.newInstance();
Singleton02 singleton023 = constructor.newInstance();
System.out.println(singleton022 == singleton023); // 輸出false 攻擊成功
} catch (Exception e){
e.printStackTrace();
}
防禦:标記位防禦
private static boolean flag = false;
private Singleton03(){
if(!flag){
flag = true;
}
else{
throw new RuntimeException("請不要進行反射攻擊");
}
}
攻擊:修改标記位
try {
Field field = Singleton03.class.getDeclaredField("flag");
field.setAccessible(true);
Constructor<Singleton03> constructor = Singleton03.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Singleton03 singleton03 = constructor.newInstance();
field.set(singleton03,false);
Singleton03 singleton031 = constructor.newInstance();
System.out.println(singleton03 == singleton031);
} catch (Exception e){
e.printStackTrace();
}
上面一連串的攻擊與防禦,可以說明對于反射攻擊,懶漢式并不安全。
最終方案:枚舉
參考連結:為什麼使用枚舉實作單例(避免序列化,反射問題)
攻擊與防禦
原始的枚舉單例
public enum Singleton {
INSTANCE;
private static Singleton getInstance(){
return INSTANCE;
}
}
第一波攻擊:使用反射構造對象
try {
Constructor<Singleton> singletonConstructor = Singleton.class.getDeclaredConstructor();
singletonConstructor.setAccessible(true);
Singleton singleton2 = singletonConstructor.newInstance();
System.out.println(singleton2 == singleton);
} catch (Exception e){
e.printStackTrace();
}
// 輸出
/*
java.lang.NoSuchMethodException: zhj.pro.reflect.Singleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at zhj.pro.reflect.Main.main(Main.java:21)
*/
可以看到,并不是反射攻擊問題,而是找不到方法,也就是說不存在無參構造方法。
看一下Enum源碼
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
/**
* The name of this enum constant, as declared in the enum declaration.
* Most programmers should use the {@link #toString} method rather than
* accessing this field.
*/
private final String name;
/**
* The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*
* Most programmers will have no use for this field. It is designed
* for use by sophisticated enum-based data structures, such as
* {@link java.util.EnumSet} and {@link java.util.EnumMap}.
*/
private final int ordinal;
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
/*more*/
分析
可以看到,構造方式含有2個參數,是以修改第一波攻擊代碼
try {
Constructor<Singleton> singletonConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
singletonConstructor.setAccessible(true);
Singleton singleton2 = singletonConstructor.newInstance();
System.out.println(singleton2 == singleton);
} catch (Exception e){
e.printStackTrace();
}
// 輸出
/*
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at zhj.pro.reflect.Main.main(Main.java:23)
*/
分析
攻擊失敗,而且是因為這類是枚舉類。
其實可以看看newInstance就會明白的——當反射在通過newInstance建立對象時,會檢查該類是否ENUM修飾,如果是則抛出異常,反射失敗。
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
//反射在通過newInstance建立對象時,會檢查該類是否ENUM修飾,如果是則抛出異常,反射失敗。
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
總結
應用
身份證
題目來源:設計模式(第2版)—單例模式—劉偉
解析
每個人的身份證是隻有一個的,當第一次生成時,即執行個體化時,身份證便會生成。
如果身份證丢失,需要重辦,不會重新配置設定一個号碼給你,而是将之前的身份證号碼發你。
編碼
使用雙重檢查實作
public class IdCard {
private IdCard(){
}
private String sno;
public String getSno() {
return sno;
}
public void setSno(String sno) {
this.sno = sno;
}
private volatile static IdCard idCard = null;
public IdCard getIdCard(){
if(idCard == null) {
synchronized (this){
if(idCard == null){
idCard = new IdCard();
}
}
}
return idCard;
}
}
測試
public class Main {
public static void main(String[] args) {
IdCard idCard01,idCard02;
idCard01 = IdCard.getIdCard();
idCard02 = IdCard.getIdCard();
System.out.println(idCard01 == idCard02);
System.out.println(idCard01.getSno());
System.out.println(idCard02.getSno());
System.out.println(idCard01.getSno().equals(idCard02.getSno()));
}
}
結果
第一次擷取身份證
重複擷取身份證
true
NO.123456
NO.123456
true
列印池
題目來源:設計模式(第2版)—單例模式—劉偉
解析
如果有2台列印機連接配接一台電腦,列印的時候,是其中一台列印即可還是兩台都列印同一份檔案?
顯然,答案是前者。因為另一台列印機也列印的話,就會造成資源浪費了。
編碼
使用靜态内部類的方式實作
public class PrintPool {
private volatile static PrintPool printPool = null;
private PrintPool(){
}
public PrintPool getInstance() throws PrintException {
if (printPool==null){
synchronized (PrintPool.class){
if(printPool==null){
printPool = new PrintPool();
}
}
}else{
throw new PrintException("列印機正在列印");
}
return printPool;
}
}
測試
public class Main {
public static void main(String[] args){
PrintPool printPool01,printPool02;
// 建立 第1個列印池
try {
printPool01 = PrintPool.getPrintPool();
} catch (PrintPoolException e){
System.out.println(e.getMessage());
}
// 建立 第2個列印池
try {
printPool02 = PrintPool.getPrintPool();
} catch (PrintPoolException e){
System.out.println(e.getMessage());
}
}
}
單例模式的影子
JDK中的單例模式
RunTime
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() {}
/*more*/
...
}
RunTime類使用的是餓漢式單例模式,
其中使用單例模式的目的在于保證每一個類隻有一個單例;
由于使用的是餓漢式單例,是以類加載器一加載類,就建立執行個體。
Spring Bean中的單例
差別
關聯的環境不同
- 單例設計模式是指在一個JVM程序中(理論上,一個運作的Java程式必定有一個自己的獨立的JVM)僅有一個執行個體;
- Spring單例是指一個Spring Bean容器中(ApplicationContext)中僅有一個執行個體
問題
在多線程的條件下,單例模式的單例是如何保證的?
多個線程調用的話,以下在程序中也隻有一個執行個體麼?
public class Singleton{
// 私有構造方法
private Singleton(){
}
// 私有自身的靜态成員變量
private static Singleton singleton = new Singleton();
// 公有靜态工廠方法
public static Singleton getInstance(){
return singleton;
}
}
問題來源:https://www.liaoxuefeng.com/wiki/1252599548343744/1281319214514210 一條評論
分析
其實在使用靜态内部類實作單例的時候就有描述過,也是同樣的處理流程。
同一個類加載器能保證類隻加載一次,且類内使用static修飾的語句隻會執行一次。
是以類
Singleton
被加載後,自身的靜态成員變量會被執行,這一次執行,就是執行個體化。
是以能夠保證類
隻有一個執行個體。
Singleton
總結
以上總結的單例模式有多少種?
首先餓漢式根據private和public的配置設定有2種;
其次,懶漢式有線程不安全與線程安全的問題。
線程不安全的有簡單的懶漢式以及使用sysnchronized修飾代碼塊的懶漢式
線程安全的有使用sysnchronized修飾方法的懶漢式與使用雙重檢查的懶漢式。
是以懶漢式有4種。
另外,還有使用靜态内部類與枚舉實作單例的2種方式。
綜上所述,這裡的單例模式有2+4+2=8種。
幾個關鍵字構成單例模式
上面講了單例模式中的餓漢式主要由private和public兩個修飾符決定的。
如果對于整個單例模式而言,單例模式可以由以下幾個關鍵字決定:
-
private和public
其中2個修飾符,已經說了,3個角色配置設定2個私有修飾符和1個公有修飾符,其中構造方法必須是private,
還剩1個私有和公有修飾符配置設定給自身靜态成員變量和靜态工廠方法。
另外,如果是靜态内部類實作單例的話,則使用私有修飾符修飾靜态内部類,最後使用公有修飾符修飾靜态工廠方法。
-
static
static關鍵字和private和public一樣重要,不過,static是貫穿整個單例模式的。
其中工廠方法
getInstance()
和自身成員變量都是使用static修飾的。
目的在于同一個類加載器加載類時隻能加載一次,且使用static修飾的方法或變量都隻能加載一次。
是以
的{修飾符} static Singleton singleton = new Singleton()
,即執行個體化,隻能夠執行一次,進而保證單例模式的産生。new Singleton()
-
sysnchronized
sysnchronized是同步鎖,保證懶漢式的線程安全的。
其中使用同步鎖修飾方法和雙重檢查能夠保證線程安全,但使用同步鎖修飾方法的性能太低了。
雙重檢查還可以。
使用同步鎖修飾代碼塊實際上也是線程不安全的。
最後懶漢式還有一種最原始的,不能夠在多線程的環境下使用,容易産生競争條件。