主頁 > 後端開發 > 我是如何用單例模式征服面試官的?

我是如何用單例模式征服面試官的?

2021-10-21 07:54:48 後端開發

前言

單例模式無論在我們面試,還是日常作業中,都會面對的問題,但很多單例模式的細節,值得我們深入探索一下,

這篇文章透過單例模式,串聯了多方面基礎知識,非常值得一讀,

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那么難了,

BAT大佬寫的刷題筆記,讓我offer拿到手軟

1 什么是單例模式?

單例模式是一種非常常用的軟體設計模式,它定義是單例物件的類只能允許一個實體存在

該類負責創建自己的物件,同時確保只有一個物件被創建,一般常用在工具類的實作或創建物件需要消耗資源的業務場景,

單例模式的特點:

  • 類構造器私有
  • 持有自己類的參考
  • 對外提供獲取實體的靜態方法

我們先用一個簡單示例了解一下單例模式的用法,

public class SimpleSingleton {
    //持有自己類的參考
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實體的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
    
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

列印結果:

1639705018
1639705018

我們看到兩次獲取SimpleSingleton實體的hashCode是一樣的,說明兩次呼叫獲取到的是同一個物件,

可能很多朋友平時作業當中都是這么用的,但我要說這段代碼是有問題的,你會相信嗎?

不信,我們一起往下看,

2 餓漢和懶漢模式

在介紹單例模式的時候,必須要先介紹它的兩種非常著名的實作方式:餓漢模式懶漢模式

2.1 餓漢模式

實體在初始化的時候就已經建好了,不管你有沒有用到,先建好了再說,具體代碼如下:

public class SimpleSingleton {
    //持有自己類的參考
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實體的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

餓漢模式,其實還有一個變種:

public class SimpleSingleton {
    //持有自己類的參考
    private static final SimpleSingleton INSTANCE;
    static {
       INSTANCE = new SimpleSingleton();
    }

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實體的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

使用靜態代碼塊的方式實體化INSTANCE物件,

使用餓漢模式的好處是:沒有執行緒安全的問題,但帶來的壞處也很明顯,

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一開始就實體化物件了,如果實體化程序非常耗時,并且最后這個物件沒有被使用,不是白白造成資源浪費嗎?

還真是啊,

這個時候你也許會想到,不用提前實體化物件,在真正使用的時候再實體化不就可以了?

這就是我接下來要介紹的:懶漢模式

2.2 懶漢模式

顧名思義就是實體在用到的時候才去創建,“比較懶”,用的時候才去檢查有沒有實體,如果有則回傳,沒有則新建,具體代碼如下:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

示例中的INSTANCE物件一開始是空的,在呼叫getInstance方法才會真正實體化,

嗯,不錯不錯,但這段代碼還是有問題,

2.3 synchronized關鍵字

上面的代碼有什么問題?

答:假如有多個執行緒中都呼叫了getInstance方法,那么都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時默認值是null,這樣會導致多個執行緒中同時創建INSTANCE物件,即INSTANCE物件被創建了多次,違背了只創建一個INSTANCE物件的初衷,

那么,要如何改進呢?

答:最簡單的辦法就是使用synchronized關鍵字,

改進后的代碼如下:

public class SimpleSingleton3 {
    private static SimpleSingleton3 INSTANCE;

    private SimpleSingleton3() {
    }

    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}

在getInstance方法上加synchronized關鍵字,保證在并發的情況下,只有一個執行緒能創建INSTANCE物件的實體,

這樣總可以了吧?

答:不好意思,還是有問題,

有什么問題?

答:使用synchronized關鍵字會消耗getInstance方法的性能,我們應該判斷當INSTANCE為空時才加鎖,如果不為空不應該加鎖,需要直接回傳,

這就需要使用下面要說的雙重檢查鎖了,

2.4 餓漢和懶漢模式的區別

but,在介紹雙重檢查鎖之前,先插播一個朋友們可能比較關心的話題:餓漢模式 和 懶漢模式 各有什么優缺點?

  • 餓漢模式:優點是沒有執行緒安全的問題,缺點是浪費記憶體空間,
  • 懶漢模式:優點是沒有記憶體空間浪費的問題,缺點是如果控制不好,實際上不是單例的,

好了,下面可以安心的看看雙重檢查鎖,是如何保證性能的,同時又保證單例的,

3 雙重檢查鎖

雙重檢查鎖顧名思義會檢查兩次:在加鎖之前檢查一次是否為空,加鎖之后再檢查一次是否為空,

那么,它是如何實作單例的呢?

3.1 如何實作單例?

具體代碼如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

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

在加鎖之前判斷是否為空,可以確保INSTANCE不為空的情況下,不用加鎖,可以直接回傳,

為什么在加鎖之后,還需要判斷INSTANCE是否為空呢?

答:是為了防止在多執行緒并發的情況下,只會實體化一個物件,

比如:執行緒a和執行緒b同時呼叫getInstance方法,假如同時判斷INSTANCE都為空,這時會同時進行搶鎖,

假如執行緒a先搶到鎖,開始執行synchronized關鍵字包含的代碼,此時執行緒b處于等待狀態,

執行緒a創建完新實體了,釋放鎖了,此時執行緒b拿到鎖,進入synchronized關鍵字包含的代碼,如果沒有再判斷一次INSTANCE是否為空,則可能會重復創建實體,

所以需要在synchronized前后兩次判斷,

不要以為這樣就完了,還有問題呢?

3.2 volatile關鍵字

上面的代碼還有啥問題?

public static SimpleSingleton4 getInstance() {
      if (INSTANCE == null) {//1
          synchronized (SimpleSingleton4.class) {//2
              if (INSTANCE == null) {//3
                  INSTANCE = new SimpleSingleton4();//4
              }
          }
      }
      return INSTANCE;//5
  }

getInstance方法的這段代碼,我是按1、2、3、4、5這種順序寫的,希望也按這個順序執行,

但是java虛擬機實際上會做一些優化,對一些代碼指令進行重排,重排之后的順序可能就變成了:1、3、2、4、5,這樣在多執行緒的情況下同樣會創建多次實體,重排之后的代碼可能如下:

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
       if (INSTANCE == null) {//3
           synchronized (SimpleSingleton4.class) {//2
                INSTANCE = new SimpleSingleton4();//4
            }
        }
    }
    return INSTANCE;//5
}

原來如此,那有什么辦法可以解決呢?

答:可以在定義INSTANCE是加上volatile關鍵字,具體代碼如下:

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

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

volatile關鍵字可以保證多個執行緒的可見性,但是不能保證原子性,同時它也能禁止指令重排,

雙重檢查鎖的機制既保證了執行緒安全,又比直接上鎖提高了執行效率,還節省了記憶體空間,

除了上面的單例模式之外,還有沒有其他的單例模式?

4 靜態內部類

靜態內部類顧名思義是通過靜態的內部類來實作單例模式的,

那么,它是如何實作單例的呢?

4.1 如何實作單例模式?

具體代碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

我們看到在SimpleSingleton5類中定義了一個靜態的內部類Inner,在SimpleSingleton5類的getInstance方法中,回傳的是內部類Inner的實體INSTANCE物件,

只有在程式第一次呼叫getInstance方法時,虛擬機才加載Inner并實體化INSTANCE物件,

java內部機制保證了,只有一個執行緒可以獲得物件鎖,其他的執行緒必須等待,保證物件的唯一性,

4.2 反射漏洞

上面的代碼看似完美,但還是有漏洞,如果其他人使用反射,依然能夠通過類的無參構造方式創建物件,例如:

Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
try {
    SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
    System.out.println(newInstance == SimpleSingleton5.getInstance());
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

上面代碼列印結果是false,

由此看出,通過反射創建的物件,跟通過getInstance方法獲取的物件,并非同一個物件,也就是說,這個漏洞會導致SimpleSingleton5非單例,

那么,要如何防止這個漏洞呢?

答:這就需要在無參構造方式中判斷,如果非空,則拋出例外了,

改造后的代碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
        if(Inner.INSTANCE != null) {
           throw new RuntimeException("不能支持重復實體化");
       }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
        }
    }

}

如果此時,你認為這種靜態內部類,實作單例模式的方法,已經完美了,

那么,我要告訴你的是,你錯了,還有漏洞,,,

4.3 反序列化漏洞

眾所周知,java中的類通過實作Serializable介面,可以實作序列化,

我們可以把類的物件先保存到記憶體,或者某個檔案當中,后面在某個時刻,再恢復成原始物件,

具體代碼如下:

public class SimpleSingleton5 implements Serializable {

    private SimpleSingleton5() {
        if (Inner.INSTANCE != null) {
            throw new RuntimeException("不能支持重復實體化");
        }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    private static void writeFile() {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
            fos = new FileOutputStream(new File("test.txt"));
            oos = new ObjectOutputStream(fos);
            oos.writeObject(simpleSingleton5);
            System.out.println(simpleSingleton5.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private static void readFile() {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            ois = new ObjectInputStream(fis);
            SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();

            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        writeFile();
        readFile();
    }
}

運行之后,發現序列化和反序列化后物件的hashCode不一樣:

189568618
793589513

說明,反序列化時創建了一個新物件,打破了單例模式物件唯一性的要求,

那么,如何解決這個問題呢?

答:重新readResolve方法,

在上面的實體中,增加如下代碼:

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

運行結果如下:

290658609
290658609

我們看到序列化和反序列化實體物件的hashCode相同了,

做法很簡單,只需要在readResolve方法中,每次都回傳唯一的Inner.INSTANCE物件即可,

程式在反序列化獲取物件時,會去尋找readResolve()方法,

  • 如果該方法不存在,則直接回傳新物件,
  • 如果該方法存在,則按該方法的內容回傳物件,
  • 如果我們之前沒有實體化單例物件,則會回傳null,

好了,到這來終于把坑都踩完了,

還是費了不少勁,

不過,我偷偷告訴你一句,其實還有更簡單的方法,哈哈哈,

納尼,,,

5 列舉

其實在java中列舉就是天然的單例,每一個實體只有一個物件,這是java底層內部機制保證的,

簡單的用法:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    public void doSamething() {
        System.out.println("doSamething");
    }
}   

在呼叫的地方:

public class SimpleSingleton7Test {

    public static void main(String[] args) {
        SimpleSingleton7.INSTANCE.doSamething();
    }
}

在列舉中實體物件INSTANCE是唯一的,所以它是天然的單例模式,

當然,在列舉物件唯一性的這個特性,還能創建其他的單例物件,例如:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    private Student instance;
    
    SimpleSingleton7() {
       instance = new Student();
    }
    
    public Student getInstance() {
       return instance;
    }
}

class Student {
}

jvm保證了列舉是天然的單例,并且不存在執行緒安全問題,此外,還支持序列化,

在java大神Joshua Bloch的經典書籍《Effective Java》中說過:

單元素的列舉型別已經成為實作Singleton的最佳方法,

6 多例模式

我們之前聊過的單例模式,都只會產生一個實體,但它其實還有一個變種,也就是我們接下來要聊的:多例模式

多例模式顧名思義,它允許創建多個實體,但它的初衷是為了控制實體的個數,其他的跟單例模式差不多,

具體實作代碼如下:

public class SimpleMultiPattern {
    //持有自己類的參考
    private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
    private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();

    //私有的構造方法
    private SimpleMultiPattern() {
    }
    //對外提供獲取實體的靜態方法
    public static SimpleMultiPattern getInstance(int type) {
        if(type == 1) {
          return INSTANCE1;
        }
        return INSTANCE2;
    }
}

為了看起來更直觀,我把一些額外的安全相關代碼去掉了,

有些朋友可能會說:既然多例模式也是為了控制實體數量,那我們常見的池技術,比如:資料庫連接池,是不是通過多例模式實作的?

答:不,它是通過享元模式實作的,

那么,多例模式和享元模式有什么區別?

  • 多例模式:跟單例模式一樣,純粹是為了控制實體數量,使用這種模式的類,通常是作為程式某個模塊的入口,
  • 享元模式:它的側重點是物件之間的銜接,它把動態的、會變化的狀態剝離出來,共享不變的東西,

7 真實使用場景

最后,跟大家一起聊聊,單例模式的一些使用場景,我們主要看看在java的框架中,是如何使用單例模式,給有需要的朋友一個參考,

7.1 Runtime

jdk提供了Runtime類,我們可以通過這個類獲取系統的運行狀態,

比如可以通過它獲取cpu核數:

int availableProcessors = Runtime.getRuntime().availableProcessors();

Runtime類的關鍵代碼如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    ...
}

從上面的代碼我們可以看出,這確實是一個單例模式,并且是餓漢模式,

但根據文章之前講過的一些理論知識,你會發現Runtime類的這種單例模式實作方式,顯然不太好,實體物件既沒用final關鍵字修飾,也沒考慮物件實體化的性能消耗問題,

不過它的優點是實作起來非常簡單,

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那么難了,

BAT大佬寫的刷題筆記,讓我offer拿到手軟

7.2 NamespaceHandlerResolver

spring提供的DefaultNamespaceHandlerResolver是為需要初始化默認命名空間處理器,是為了方便后面做標簽決議用的,

它的關鍵代碼如下:

@Nullable
private volatile Map<String, Object> handlerMappings;

private Map<String, Object> getHandlerMappings() {
		Map<String, Object> handlerMappings = this.handlerMappings;
		if (handlerMappings == null) {
			synchronized (this) {
				handlerMappings = this.handlerMappings;
				if (handlerMappings == null) {
					if (logger.isDebugEnabled()) {
						logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
					}
					try {
						Properties mappings =
								PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
						if (logger.isDebugEnabled()) {
							logger.debug("Loaded NamespaceHandler mappings: " + mappings);
						}
						handlerMappings = new ConcurrentHashMap<>(mappings.size());
						CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
						this.handlerMappings = handlerMappings;
					}
					catch (IOException ex) {
						throw new IllegalStateException(
								"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
					}
				}
			}
		}
		return handlerMappings;
	}

我們看到它使用了雙重檢測鎖,并且還定義了一個區域變數handlerMappings,這是非常高明之處,

使用區域變數相對于不使用區域變數,可以提高性能,主要是由于 volatile 變數創建物件時需要禁止指令重排序,需要一些額外的操作,

7.3 LogFactory

mybatis提供LogFactory類是為了創建日志物件,根據引入的jar包,決定使用哪種方式列印日志,具體代碼如下:

public final class LogFactory {

  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}

這段代碼非常經典,但它卻是一個不走尋常路的單例模式,因為它創建的實體物件,可能存在多種情況,根據引入不同的jar包,加載不同的類創建實體物件,如果有一個創建成功,則用它作為整個類的實體物件,

這里有個非常巧妙的地方是:使用了很多tryImplementation方法,方便后面進行擴展,不然要寫很多,又臭又長的if…else判斷,

此外,它跟常規的單例模式的區別是,LogFactory類中定義的實體物件是Log型別,并且getLog方法回傳的引數型別也是Log,不是LogFactory,

最關鍵的一點是:getLog方法中是通過構造器的newInstance方法創建的實體物件,每次請求getLog方法都會回傳一個新的實體,它其實是一個多例模式,

7.4 ErrorContext

mybatis提供ErrorContext類記錄了錯誤資訊的背景關系,方便后續處理,

那么它是如何實作單例模式的呢?關鍵代碼如下:

public class ErrorContext {
  ...
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
  
  private ErrorContext() {
  }
  
  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
  ...
}  

我們可以看到,ErrorContext跟傳統的單例模式不一樣,它改良了一下,它使用了餓漢模式,并且使用ThreadLocal,保證每個執行緒中的實體物件是單例的,這樣看來,ErrorContext類創建的物件不是唯一的,它其實也是多例模式的一種,

7.5 spring的單例

以前在spring中要定義一個bean,需要在xml檔案中做如下配置:

<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">

在bean標簽上有個scope屬性,我們可以通過指定該屬性控制bean實體是單例的,還是多例的,如果值為singleton,代表是單例的,當然如果該引數不指定,默認也是單例的,如果值為prototype,則代表是多例的,

在spring的AbstractBeanFactory類的doGetBean方法中,有這樣一段代碼:

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      return createBean(beanName, mbd, args);
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
    Object prototypeInstance = createBean(beanName, mbd, args);
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
    ....
}

這段代碼我為了好演示,看起來更清晰,我特地簡化過的,它的主要邏輯如下:

  1. 判斷如果scope是singleton,則呼叫getSingleton方法獲取實體,
  2. 如果scope是prototype,則直接創建bean實體,每次會創建一個新實體,
  3. 如果scope是其他值,則允許我們自定bean的創建程序,

其中getSingleton方法主要代碼如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(beanName, "Bean name must not be null");
		synchronized (this.singletonObjects) {
			Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
          singletonObject = singletonFactory.getObject();
         if (newSingleton) {
					      addSingleton(beanName, singletonObject);
				    }
			}
			return singletonObject;
		}
}

有個關鍵的singletonObjects物件,其實是一個ConcurrentHashMap集合:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

getSingleton方法的主要邏輯如下:

  1. 根據beanName先從singletonObjects集合中獲取bean實體,
  2. 如果bean實體不為空,則直接回傳該實體,
  3. 如果bean實體為空,則通過getObject方法創建bean實體,然后通過addSingleton方法,將該bean實體添加到singletonObjects集合中,
  4. 下次再通過beanName從singletonObjects集合中,就能獲取到bean實體了,

在這里spring是通過ConcurrentHashMap集合來保證物件的唯一性,

最后留給大家幾個小問題思考一下:

  1. 多例模式 和 多物件模式有什么區別?
  2. java框架中有些單例模式用的不規范,我要參考不?
  3. spring的單例,只是結果是單例的,但完全沒有遵循單例模式的固有寫法,它也算是單例模式嗎?

歡迎大家給我留言,說出你心中的答案,

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想象中那么難了,

BAT大佬寫的刷題筆記,讓我offer拿到手軟

碼字不易,如果讀了文章有些識訓的話,請幫我點贊一下,謝謝你的支持和鼓勵,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/327981.html

標籤:java

上一篇:cgb2108-day15

下一篇:第十二屆藍橋杯 2021年國賽真題 (Java 大學B組)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more