孔乙己是如此地讓人快活,可是沒有他,人們也照樣過,有一天,大約是中秋的前兩天,一個面試官說,孔乙己呢,他還欠我一個懶漢單例呢?我也才覺著他好久都沒手撕代碼了,一個經常抖機靈的說,他怎么會來?賠了個一干二凈,面試官說哦?“他總是被面試八股,背昏了頭,幾天前竟然在牛客網里曬網易面試的面經,丁舉人是學法律的,現在是資深老律師,自然起訴孔乙己,到最后孔乙己刪了文章,賠了500袋茴香豆,”“后來呢?” “怕是退出了春招吧”
不抖機靈了
還是說回單例模式吧
我參加的春招的每場面試 但凡手撕代碼了 都要手撕單例模式
對于從來沒系統學過 多執行緒并發 和 23種常用設計模式的我 可謂是一下難死了
不過 單例模式也不是很難嘛 很容易就能從網上查到十種 單例模式的寫法
看著 餓漢 懶漢也不是很難,但是 跟面試官一面對面 手撕的時候就緊張的 一行代碼敲不出來
我把可能會 手撕的代碼 傳到了 github 上作為筆記 當然還沒更新完
昨天一天 也就學了一個 單例模式而已
總結一下吧 其實也不是很難 只是自己太懶
https://github.com/HANXU2018/shousi
文章目錄
- 單例模式是個啥
- 單例模式的九種寫法
- 1. 餓漢式單例
- 【第一種】靜態代碼 生成單例
- 【第二種】靜態代碼塊生成單例
- 評價
- 2. 懶漢式單例
- 【第三種】執行緒不安全單例
- 【第四種】synchronized 懶漢單例
- DCL 雙鎖機制存在的問題
- 【第五種】內部類懶漢單例
- 【第六種】不允許反射破壞的內部類懶漢單例
- 【第七種】不允許序列化破壞的內部類懶漢單例
- 3. 注冊式單例
- 【第八種】注冊式spring容器單例
- 4. ThreadLocal 式單例
- 【第九種】ThreadLocal執行緒私有 式單例
- 5. 列舉類 單例
- 【第十種】JDK保護的列舉類單例
- 總結下吧
單例模式是個啥
-
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一,這種型別的設計模式屬于創建型模式,它提供了一種創建物件的最佳方式,
-
這種模式涉及到一個單一的類,該類負責創建自己的物件,同時確保只有單個物件被創建,這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要實體化該類的物件,
注意:
1、單例類只能有一個實體,
2、單例類必須自己創建自己的唯一實體,
3、單例類必須給所有其他物件提供這一實體,
關鍵詞:創建型模式、單一物件、訪問唯一物件、無需實體化
菜鳥教程相關 檔案
-
意圖:保證一個類僅有一個實體,并提供一個訪問它的全域訪問點,
-
主要解決:一個全域使用的類頻繁地創建與銷毀,
-
何時使用:當您想控制實體數目,節省系統資源的時候,
-
如何解決:判斷系統是否已經有這個單例,如果有則回傳,如果沒有則創建,
-
關鍵代碼:建構式是私有的,
-
應用實體:
1、一個班級只有一個班主任,
2、Windows 是多行程多執行緒的,在操作一個檔案的時候,就不可避免地出現多個行程或執行緒同時操作一個檔案的現象,所以所有檔案的處理必須通過唯一的實體來進行,
3、一些設備管理器常常設計為單例模式,比如一個電腦有兩臺列印機,在輸出的時候就要處理不能兩臺列印機列印同一個檔案, -
優點:
1、在記憶體里只有一個實體,減少了記憶體的開銷,尤其是頻繁的創建和銷毀實體(比如管理學院首頁頁面快取),
2、避免對資源的多重占用(比如寫檔案操作),
缺點:沒有介面,不能繼承,與單一職責原則沖突,一個類應該只關心內部邏輯,而不關心外面怎么樣來實體化, -
使用場景:
1、要求生產唯一序列號,
2、WEB 中的計數器,不用每次重繪都在資料庫里加一次,用單例先快取起來,
3、創建的一個物件需要消耗的資源過多,比如 I/O 與資料庫的連接等,
注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多執行緒同時進入造成 instance 被多次實體化,
注意建構式私有 我手撕代碼的時候竟然忘記了,不應該呀 不應該~
單例模式的九種寫法
筆記 整理了九種 單例模式寫法
分為這
- 餓漢式單例
- 【第一種】靜態代碼 生成單例
- 【第二種】靜態代碼塊生成單例
- 懶漢式單例
- 【第三種】執行緒不安全單例
- 【第四種】synchronized 懶漢單例
- 【第五種】內部類懶漢單例
- 【第六種】不允許反射破壞的內部類懶漢單例
- 【第七種】不允許序列化破壞的內部類懶漢單例
- DCL 雙鎖機制存在的問題
- 注冊式單例
- 【第八種】注冊式spring容器單例
- ThreadLocal 式單例
- 【第九種】ThreadLocal執行緒私有 式單例
- 列舉類 單例
- 【第十種】JDK保護的列舉類單例
1. 餓漢式單例
【第一種】靜態代碼 生成單例
這種方式是最基本的實作方式,這種實作最大的問題就是不支持多執行緒,
因為沒有加鎖 synchronized,所以嚴格意義上它并不算單例模式,
這種方式 lazy loading 很明顯,不要求執行緒安全,在多執行緒不能正常作業,
注意不要忘了 private HungrySingleton() { } 面試的時候我就忘了就很難受
package singleton.hungry;
public class HungrySingleton {
private static final HungrySingleton hungrySigleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySigleton;
}
}
【第二種】靜態代碼塊生成單例
在第一種的基礎上 把原來的 初始化賦值 改成了 在 static 靜態塊里面操作
package singleton.hungry;
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySigleton;
static {
hungrySigleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance(){
return hungrySigleton;
}
}
評價
- 優點:創建物件時沒有加任何的鎖、執行效率比較高,
- 缺點:也很明顯,因為其在類加載的時候就初始化了,也就是說不管我們用或者不用都占著空間,如果專案中有大量單例物件,則可能會浪費大量記憶體空間,
2. 懶漢式單例
懶漢單例在 修改了 餓漢單例變成了 延時加載
需要的時候才初始化
【第三種】執行緒不安全單例
如果兩個執行緒同時走到 if(null == lazySingleton)
就會 創建多個實體 就破壞了單例模式
package singleton.lazy;
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(null == lazySingleton){//為空則說明第一次獲取單例物件,進行初始化
lazySingleton = new LazySingleton();
}
return lazySingleton;//不為空則說明已經初始化了,直接回傳
}
}
【第四種】synchronized 懶漢單例
在第三種的基礎上 加入
public synchronized static LazySyncSingleton getInstance(){
這樣獲取單例的時候就避免了 剛才說的那個 執行緒安全問題
但是讓大家都卡在 getInstance 這里 效率也太低了吧
我們 能不能 只 鎖 if(null == lazySingleton){ 這里的代碼?
package singleton.lazy;
public class LazySyncSingleton {
private static LazySyncSingleton lazySingleton = null;
private LazySyncSingleton() {
}
public synchronized static LazySyncSingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySyncSingleton();
}
return lazySingleton;
}
}
DCL 雙鎖機制存在的問題
這才是面試官想要看到的代碼
必追問的內容 為什么兩層 if 判斷 null == lazySingleton
package singleton.lazy;
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if(null == lazySingleton){
synchronized (LazyDoubleCheckSingleton.class){
if(null == lazySingleton){
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
雙重檢查鎖(double-checked locking) 除了兩層if陳述句 還加入了 volatile 關鍵字
在 private volatile static LazyDoubleCheckSingleton lazySingleton = null; 上面
補充一下 volatile
volatile關鍵字為域變數的訪問提供了一種免鎖機制,
使用volatile修飾域相當于告訴虛擬機該域可能會被其他執行緒更新,
因此每次使用該域就要重新計算,
而不是使用暫存器中的值,
需要注意的是,
volatile不會提供任何原子操作,
它也不能用來修飾final型別的變數,
關鍵資訊提取一下 volatile 免鎖 不快取 不修飾 final 不是原子的 保證資料可見性 避免代碼重排
第一使用 volatile 解決多執行緒下的可見性問題,
- 因為我們的 getInstance 方法在判斷 lazySingleton 是否為 null 時候并沒有加鎖,
- 所以假如執行緒 t1 初始化過了物件,另外執行緒如 t2 是無法感知的,而加上了 volatile 就可以感知到,
第二把 synchronized 關鍵字移到了方法內部,盡可能縮小加鎖的代碼塊,提升效率,
我以為已經很厲害了這樣
結果還是有 bug 指令重排還存在著
new 物件的順序
- 分配記憶體來創建物件,即:new,
- 創建一個物件 lazySingleton,此時 lazySingleton == null,
- 將 new 出來的物件賦值給 lazySingleton,
- 實際運行的時候為了提升效率,
- 這 3 步并不會按照實際順序來運行的,
- 那我們打個比方,假如有一個執行緒 t1 進入同步代碼塊正在創建物件,
- 而此時執行了上面 3 個步驟中的后面 2 步,
- 也就是說這時候 lazySingleton 已經不為 null 了,
- 但是物件卻并沒有創建結束;
- 此時又來了一個執行緒 t2 進入 getInstance 方法,
- 這時候 if 條件肯定不成了,
- 執行緒 t2 會直接回傳,
- 也就相當于回傳了一個殘缺不全的物件,
- 這時候代碼就會報錯了
那還是看看 下面別的 單例模式方法吧
【第五種】內部類懶漢單例
package singleton.lazy;
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
}
public static final LazyInnerClassSingleton getInstance(){
return InnerLazy.LAZY;
}
private static class InnerLazy{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
利用了內部類會等到外部呼叫時才會被初始化的特性,
用餓漢式單例的思想實作了懶漢式單例
餓漢單例思想 實作 懶漢式單例 感覺太厲害了 前人的智慧總是很厲害的,
【第六種】不允許反射破壞的內部類懶漢單例
前面說的很厲害
但是 Java 的 反射更厲害
單例 模式說破壞就破壞
constructor.setAccessible(true);
private 的限制說沒就沒
package singleton.lazy;
import java.lang.reflect.Constructor;
public class TestLazyInnerClassSingleton {
public static void main(String[] args) throws Exception {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2 = LazyInnerClassSingleton.getInstance();
System.out.println(o1 == o2);
}
}
上有政策下有對策
Java 既是矛又是盾 有槍也有保護罩
既然 破壞單例要 執行構造方法 constructor.newInstance();
那么我們 先把 構造方法攔截住
package singleton.lazy;
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
//防止反射破壞單例
if(null != InnerLazy.LAZY){
throw new RuntimeException("不允許通過反射類構造單例物件");
}
}
public static final LazyInnerClassSingleton getInstance(){
return InnerLazy.LAZY;
}
private static class InnerLazy{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
【第七種】不允許序列化破壞的內部類懶漢單例
除了反射會破壞 單例
序列化把java物件 保存到本地 再加載到記憶體 仍然會破壞單例模式
單例模式 用了 implements Serializable 實作了序列化 也把 破壞單例的 問題帶來了
補充知識點 Extends 和 implements
Extends 可以理解為全盤繼承了父類的功能,
implements 可以理解為為這個類附加一些額外的功能;
interface 定義一些方法,并沒有實作,需要 implements 來實作才可用,
extend 可以繼承一個介面,但仍是一個介面,也需要 implements 之后才可用,
對于 class 而言,Extends 用于(單)繼承一個類(class),
而 implements 用于實作一個介面(interface),
package singleton.lazy;
import java.io.Serializable;
public class LazyInnerClassSingleton implements Serializable {
private LazyInnerClassSingleton(){
//防止反射破壞單例
if(null != InnerLazy.LAZY){
throw new RuntimeException("不允許通過反射類構造單例物件");
}
}
public static final LazyInnerClassSingleton getInstance(){
return InnerLazy.LAZY;
}
private static class InnerLazy {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
我們用輸入輸出流來 破壞這個 單例
package singleton.lazy;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestLazyInnerClassSingleton2 {
public static void main(String[] args) {
//序列化攻擊內部類式單例
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("LazyInnerClassSingleton.text");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.text");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton)ois.readObject();
ois.close();
System.out.println(s1 == s2);//輸出:false
}catch (Exception e){
e.printStackTrace();
}
}
}
補充一下流的方法
java.io.ObjectOutputStream.flush() 方法重繪流,
這將寫入所有緩沖的輸出位元組,并重繪到基礎流,
這個問題 咋解決呢 ?
在單例類的代碼加入 readResolve() 方法就行
package singleton.lazy;
import java.io.Serializable;
public class LazyInnerClassSingleton implements Serializable {
private LazyInnerClassSingleton(){
//防止反射破壞單例
if(null != InnerLazy.LAZY){
throw new RuntimeException("不允許通過反射類構造單例物件");
}
}
public static final LazyInnerClassSingleton getInstance(){
return InnerLazy.LAZY;
}
private static class InnerLazy {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
private Object readResolve(){
return InnerLazy.LAZY;
}
}
JDK 原始碼中在序列化的時候會檢驗一個類中是否存在一個 readResolve 方法,
如果存在,則會放棄通過序列化產生的物件,而回傳原本的物件,
技術上 沒有 銀彈 啊
這種方式雖然保證了單例,
但是在校驗是否存在 readResolve 方法前還是會產生一個物件,
只不過這個物件會在發現類中存在 readResolve 方法后丟掉,
然后回傳原本的單例物件,
這種寫法只是保證了結果的唯一,
但是程序中依然會被實體化多次,
假如創建物件的頻率增大,
就意味著記憶體分配的開銷也隨之增大,
3. 注冊式單例
spring 的 單例 bean 原來就是這么來的
技術都是 藕斷絲連 回圈交錯的
【第八種】注冊式spring容器單例
注冊式單例就是將每一個實體都保存起來,
然后在需要使用的時候直接通過唯一的標識獲取實體,
- private static Map<String,Object> ioc = new ConcurrentHashMap<>();//存盤單例物件
- 用 getBean 獲取 Bean 單例物件
注入 的 代碼
obj = Class.forName(className).newInstance();
ioc.put(className,obj);//將className作為唯一標識存入容器 - 單例直接從 map 里拿
return ioc.get(className);//如果容器中已經存在了單例物件,則直接回傳
package singleton.register;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();//存盤單例物件
public static Object getBean(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){//如果容器中不存在當前物件
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);//將className作為唯一標識存入容器
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
return ioc.get(className);//如果容器中已經存在了單例物件,則直接回傳
}
}
}
相關 測驗代碼
單例類
package singleton.register;
public class MyObject {
}
單元測驗
package singleton.register;
public class TestContainerSingleton {
public static void main(String[] args) {
MyObject myObject1 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");
MyObject myObject2 = (MyObject) ContainerSingleton.getBean("singleton.register.MyObject");
System.out.println(myObject1 == myObject2);//輸出:true
}
}
ioc 加入了 synchronized 關鍵字 如果不加入 默認是執行緒不安全的
4. ThreadLocal 式單例
ThreadLocal 式單例不能保證其創建的物件是全域唯一,
但是能保證在單個執行緒中是唯一的,
在單執行緒環境下執行緒天生安全,
【第九種】ThreadLocal執行緒私有 式單例
獲得的單例是 private static final ThreadLocal singleton
package singleton.thread;
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}
private static final ThreadLocal<ThreadLocalSingleton> singleton =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return singleton.get();
}
}
測驗代碼
package singleton.thread;
public class TestThreadLocalSingleton {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());//主執行緒輸出
System.out.println(ThreadLocalSingleton.getInstance());//主執行緒輸出
Thread t1 = new Thread(()-> {
ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);//t1執行緒輸出
});
t1.start();
}
}
ThreadLocal 式示例僅對單執行緒是安全的
5. 列舉類 單例
【第十種】JDK保護的列舉類單例
單例類
package singleton.meiju;
public class MyObject {
}
列舉單例
package singleton.meiju;
public enum EnumSingleton {
INSTANCE;
private MyObject myObject;
EnumSingleton() {
this.myObject = new MyObject();
}
public Object getData() {
return myObject;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
用列舉型別 來拿到 這個傳統的單例物件
嘗試反射破壞
package singleton.meiju;
import java.lang.reflect.Constructor;
public class TestEnumSingleton1 {
public static void main(String[] args) throws Exception{
//測驗反射是否可以破壞列舉式單例
Class clazz = EnumSingleton.class;
Constructor c1 = clazz.getDeclaredConstructor();//無參構造器
System.out.println(c1.newInstance());
}
}
編譯失敗 沒有無參構造方法
反編譯后的列舉原始碼 發現無參構造方法是 假的
真實底層 是 string int 兩個引數

修改后 反射還是被拒絕了
package DesignPattern.singletion.EnumSingleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class TestEnumSingleton1 {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazzz = EnumSingleton.class;
// Constructor c1 = clazzz.getDeclaredConstructor();
Constructor c2 = clazzz.getDeclaredConstructor(String.class,int.class);
c2.setAccessible(true);
// JDK 底層在保護我們的列舉類不允許被反射創建
// System.out.println(c2.newInstance("測驗",666));
// System.out.println(c1.newInstance());
/*
@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)
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;
}
*/
}
}
通過查看 反射原始碼 newInstance 的 JDK 代碼 發現 JDK 底層在保護我們的列舉類不允許被反射創建

那再 試試用 序列化破壞這個 列舉單例
package singleton.meiju;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestEnumSingleton2 {
public static void main(String[] args) throws Exception{
//測驗序列化是否可以破壞列舉式單例
EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.getInstance();
FileOutputStream fos = new FileOutputStream("EnumSingleton.text");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.text");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (EnumSingleton)ois.readObject();
ois.close();
fis.close();
System.out.println(s1.getData() == s2.getData());//true
}
}
序列化也不能破壞我們單例,
這是因為,
在 Java 規范中規定了每個列舉型別及其定義的列舉變數在 JVM 中都必須是唯一的,
因此在列舉物件的序列化僅僅是將列舉物件的屬性輸出到結果中,
反序列化的時候則是通過 valueOf 方法來查找列舉物件,
列舉式單例之所以能成為最優雅的一種寫法,原因就是 JDK 底層已經幫我們保證了不允許反射,也確保了序列化方式獲得的物件仍然唯一,
總結下吧
手撕代碼 看著代碼行數也不多
門道倒是不少
特別是這個單例模式
我隨便 在網上看看 竟然筆記里 整理了 零零碎碎 十種型別
真正掌握這些技識訓是要靠平時的不斷練習和總結
任重而道遠
手撕代碼的練習 代碼 都放倉庫里了以后也可以用來 翻一翻
感興趣的可以 clone 下來看看
https://github.com/HANXU2018/shousi
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/274855.html
標籤:其他
