單例模式
定義:保證一個類僅有一個實例,並提供一個全局的訪問點
使用場景:
- 想確保任何情況下都絕對只有一個實例
- 當你想控制實例數目,節省系統資源的時候
優點:
- 在內存裏只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷燬實例(比如管理學院首頁頁面緩存)。
- 避免對資源的多重佔用(比如寫文件操作)
- 設置全局訪問點,嚴格控制訪問
缺點:
- 沒有接口,不能繼承
- 與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化
注意事項:
- 私有構造器
- 線程安全
- 延遲加載(在第一次需要的時候再創建實例,避免不必要的內存浪費)
- 序列化和反序列化安全(需要在單例類裏,添加readResolve()方法)
- 防止反射攻擊(解決辦法:在私有構造器裏面,添加判斷,拋出異常)
不同版本的單例模式:
1.餓漢式:在類加載的時候就new出一個可供全局的實例
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){ // 防止反射創建新的實例
if(hungrySingleton != null){
throw new RuntimeException("單例模式禁止反射調用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){ //序列化和反序列化安全
return hungrySingleton;
}
}
2.懶漢式:延遲加載,在第一次需要的時候,創建實例
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("單例模式禁止反射調用");
}
}
public synchronized static LazySingleton getInstance(){ // 線程安全
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
3.懶漢式之雙重校檢鎖:
public class LazyDoubleCheckSingleton {
/**
* 注意實例變量要加volatile修飾,防止指令重排
* 新建一個對象實例有三個步驟:
* 1.分配內存給這個對象
* 2.初始化對象
* 3.設置 lazyDoubleCheckSingleton 指向剛分配的內存地址
* 但是 JVM 可能會對編譯的指令進行重排序,如:上面的三個步驟順序可能變爲 1->3->2
* 當一個線程 Thread1 新建實例時,執行到上面重排序的順序步驟 3 (此時還未執行2,即還沒有初始化)
* 而另一個線程 Thread2 調用 getInstance() 方法時,發現 lazyDoubleCheckSingleton 已經創建,不爲空
* 所以 Thread2 就會引用 lazyDoubleCheckSingleton ,但此時實例還未初始化,會報錯
*/
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
if(lazyDoubleCheckSingleton != null){
throw new RuntimeException("單例模式禁止反射調用");
}
}
public static LazyDoubleCheckSingleton getInstance(){
if (lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//1.分配內存給這個對象
//2.初始化對象
//3.設置 lazyDoubleCheckSingleton 指向剛分配的內存地址
}
}
}
return lazyDoubleCheckSingleton;
}
}
4.枚舉類單例模式,推薦使用
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
5.靜態內部類,推薦使用
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("單例模式禁止反射調用");
}
}
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
6.容器式單例模式
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> singletonMap = new HashMap<String, Object>();
public static void putInstance(String key,Object instance){
if (StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
7.使用 ThreadLocal 的單例模式
/**
* ThreadLocal 多個線程之間是不同的實例,但是線程裏面實例唯一
**/
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal = new ThreadLocal<ThreadLocalInstance>(){
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance(){
}
public static ThreadLocalInstance getInstance(){
return threadLocalInstanceThreadLocal.get();
}
}
源碼用處:
- jdk1.8 的 Runtime類(餓漢式):private static Runtime currentRuntime = new Runtime();
- jdk1.8 的 Desktop(容器式)
- spring 中的 bean 的作用域 singleton
- mybatis 中的 ErrorContext 類 (用到了ThreadLocal類型的單例模式)