Java中單例模式定義:“一個類有且僅有一個實例,並且自行實例化向整個系統提供。”
單例模式應用的場景一般發現在以下條件下:
(1)資源共享的情況下,避免由於資源操作時導致的性能或損耗等。如上述中的日志文件,應用配置。
(2)控制資源的情況下,方便資源之間的互相通信。如線程池等。
第一種 餓漢模式
定義:在類加載的時候就立即初始化,並且創建單例對象。絕對線程安全,在線程還沒出現以前就是實例化了,不可能存在訪問安全問題。
優點:沒有加任何的鎖、執行效率比較高,在用戶體驗上來說,比懶漢式更好。
缺點:類加載的時候就初始化,不管用與不用都占着空間,浪費了內存。
Spring 中 IOC 容器 ApplicationContext 本身就是典型的餓漢式單例。
public class HungrySingleton { public static HungrySingleton hungrySingleton = null; static { hungrySingleton = new HungrySingleton(); } private HungrySingleton() { } public HungrySingleton getInstance() { return hungrySingleton; } }
第二種 懶漢式單例
懶漢式單例的特點是:被外部類調用的時候內部類才會加載。
public class LazySingleton { public static LazySingleton lazySingleton = null; private LazySingleton() { } //synchronized 解決多線程並發安全問題 public synchronized static LazySingleton getInstance() { if (lazySingleton == null) { lazySingleton = new LazySingleton(); } return lazySingleton; } }
第三種 雙重檢查鎖單例
線程安全的問題雖然解決了。但是,用synchronized 加鎖,在線程數量比較多情況下,如果 CPU 分配壓力上升,會導致大批量線程出現阻塞,從而導致程序運行性能大幅下降。
那么,有沒有一種更好的方式,既兼顧線程安全又提升程序性能呢?答案是肯定的。我們來看雙重檢查鎖的單例模式:
public class DoubleLockCheckSingleton { // volatile可以實現多線程的可見性 有序性 但是不保證一致性 public volatile static DoubleLockCheckSingleton doubleLockCheckSingleton = null; private DoubleLockCheckSingleton() {} public static DoubleLockCheckSingleton getInstance() { // 最外成if 過濾掉不為null 的線程 提高效率 if (doubleLockCheckSingleton == null) { synchronized (DoubleLockCheckSingleton.class) { if (doubleLockCheckSingleton == null) { doubleLockCheckSingleton = new DoubleLockCheckSingleton(); } } } return doubleLockCheckSingleton; } }
第四種 內部類單例
雖然雙重檢查鎖確實優化了很多,但是,用到 synchronized 關鍵字,總歸是要上鎖,對程序性能還是存在一定影響的。難道就真的沒有更好的方案嗎?
當然是有的。我們可以從類初始化角度來考慮,
內部類一定是要在方法調用之前初始化,巧妙地避免了線程安全問題。
/** * 內部類 線程安全 懶漢式 * 完美的解決類懶漢時的內存浪費和synchronized的性能問題 */ public class InnerClassSingleton { private InnerClassSingleton () { // 防止被反射破壞 if (InnerClassHolder.innerClassSingleton != null) { throw new RuntimeException("不允許創建多實例"); } } // static為了使用單利內存共性 final為了讓方法不被重寫 重載 public static final InnerClassSingleton getInstance() { return InnerClassHolder.innerClassSingleton; } // 默認不加載 使用的時候才會加載 private static class InnerClassHolder { private static final InnerClassSingleton innerClassSingleton = new InnerClassSingleton(); } }
第五種 注冊式單例之枚舉式單例
注冊式單例又稱為登記式單例,就是將每一個實例都登記到某一個地方,使用唯一的標識獲取實例。
注冊式單例有兩種寫法:一種為容器緩存,一種為枚舉登記。先來看枚舉式單例的寫法,來看代碼:
public enum EnumSingleton { INSTANCE; public static EnumSingleton getInstance() { return INSTANCE; } }
第六種 注冊式單例之容器緩存
容器式寫法適用於創建實例非常多的情況,便於管理。
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ContainerSingleton { private ContainerSingleton() {} public static Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getInstance(String className) { synchronized (ioc) { Object obj = null; if (!ioc.containsKey(className)) { try { obj = Class.forName(className).newInstance(); ioc.put(className, obj); } catch (Exception e) { e.printStackTrace(); } return obj; } else { return ioc.get(className); } } } }
第七種 ThreadLocal 線程單例 (注意是線程單例)
ThreadLocal 不能保證其創建的對象是全局唯一,但是能保證在單個線程中是唯一的,天生的線程安全。下面我們來看代碼:
那么 ThreadLocal 是如果實現單例呢?我們知道上面的單例模式為了達到線程安全的目的,給方法上鎖,以時間換空間。
ThreadLocal將所有的對象全部放在 ThreadLocalMap 中,為每個線程都提供一個對象,實際上是以空間換時間來實現線程間隔離的 。
/** * 每個線程內 獨立單例 */ public class ThreadLocalSingleton { private ThreadLocalSingleton() {} public static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = ThreadLocal.withInitial(() -> new ThreadLocalSingleton()); public static ThreadLocalSingleton getInstance() { return threadLocalSingleton.get(); } }
單例模式總結
單例模式可以保證內存里只有一個實例,減少了內存開銷;可以避免對資源的多重占用。