一、單例模式介紹
1、定義與型別
定義:保證一個類僅有一個實體,并提供一個全域訪問點
型別:創建型
2、適用場景
想確保任何情況下都絕對只有一個實體
3、優點
在記憶體里只有一個實體,減少了記憶體開銷
可以避免對資源的多重占用
設定全域訪問點,嚴格控制訪問
4、缺點
沒有介面,擴展困難
5、重點
私有構造器:禁止從單例類外部構造物件
執行緒安全
延遲加載:使用時才創建
序列化和反序列化安全:序列化和反序列化會對單例模式進行破壞
反射:防御反射攻擊
二、代碼示例
1、懶漢式及多執行緒
注重延遲加載:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
但是存在執行緒安全問題,所以可以增加synchronized:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
2、Double Check雙重檢查
但是 synchronized 對性能存在影響,所以可以使用Double Check雙重檢查:
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
其中
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
這一句代碼包含三個步驟:
1.分配記憶體給這個物件
2.初始化物件
3.設定lazyDoubleCheckSingleton 指向剛分配的記憶體地址
在java語言規范中 允許在單執行緒內,不會改變單執行緒執行結果的重排序,
所以 2和3步可能會存在指令重排序,在單執行緒中,不會影響執行結果:

此時在多執行緒中:

此時執行緒1訪問物件,但是物件在執行緒0中還沒有初始化完成,可能就會報例外,
解決方案:
方案1、不允許2、3步驟重排序:
使用volatile關鍵字:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
使用了volatile后,所有執行緒都可以看到共享記憶體的最新狀態,保證了記憶體的可見性,用volatile關鍵字修飾的變數,在進行寫操作時,會多出一些匯編代碼,將當前處理器快取行的資料寫回到記憶體,其中涉及到快取一致性協議,
方案2、允許重排序,但不允許其他執行緒看到這個重排序,即靜態內部類
3、靜態內部類
基于類初始化的延遲加載解決方案
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
}
}
原理:存在Class物件的初始化鎖,并且非構造執行緒,是看不到指令重排序的,
執行緒0初始化Class,執行緒1看不到初始化程序,所以靜態內部類這種方法的核心在于InnerClass這個類的物件初始化鎖

補充:類在以下幾種情況下被初始化,1.實體被創建(new、反射、序列化),2.靜態方法被呼叫,3.靜態成員被賦值,4.非常量靜態成員被使用,5.頂級類中有嵌套的斷言陳述句,6.子類被初始化
4、餓漢式
最簡單的寫法:
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
優點是類加載的時候就完成了初始化,避免了執行緒同步的問題
缺點是沒有延遲加載的效果,可能造成累成記憶體浪費
餓漢與懶漢之間最大的區別就是延遲加載:餓漢式很餓,一上來就想吃東西,馬上就把物件創建好了;而懶漢式非常懶,不用它的時候都不會創建這個物件,
5、序列化破壞單例模式
以下序列化和反序列化 將會破壞單例模式:
// 實作序列化介面
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
測驗類:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
// 將會輸出兩個不同的記憶體地址
System.out.println(instance);
System.out.println(newInstance);
}
}
解決方法:反序列化是通過反射生成物件,在這個程序中,會判斷是否存在并呼叫readResolve方法

所以可通過增加readResolve方法防止反序列化:
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object reaResolve(){
// 回傳單例物件
return hungrySingleton;
}
}
但是在這個程序中,仍然被創建了新的物件,只是最后沒有回傳而已,
6、反射攻擊
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
// 輸出false
System.out.println(instance == newInstance);
}
}
對于餓漢式單例、靜態內部類單例,因為是在類初始化時就創建了物件,所以可在構造器中進行反射防御:
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;
}
}
而對于不是在類初始化時創建物件的單例模式,則無法防御反射攻擊,例如懶漢式單例模式:
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;
}
}
因為在被反射攻擊的時候,單例可能還沒有被創建,所以會產生不同實體,測驗類:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 反射攻擊
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
// 先反射
LazySingleton newInstance = declaredConstructor.newInstance();
// 后取單例,因為類中的實體仍為null,所以構造器的判斷沒有起到想要的作用
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
可以增加信號量進行控制:
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private static boolean flag = true;
private LazySingleton(){
if (flag){
flag = false;
} else {
throw new RuntimeException("單例構造器禁止反射呼叫");
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
但是信號量仍然可以被修改,以達到反射攻擊:
public class Test {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException {
Class objectClass = LazySingleton.class;
Constructor c = objectClass.getDeclaredConstructor();
c.setAccessible(true);
LazySingleton o1 = LazySingleton.getInstance();
Field flag = o1.getClass().getDeclaredField("flag");
flag.setAccessible(true);
// 修改信號量
flag.set(o1,true);
LazySingleton o2 = (LazySingleton) c.newInstance();
System.out.println(o1);
System.out.println(o2);
// 回傳false
System.out.println(o1==o2);
}
}
7、Enum列舉單例
列舉型別天然的可序列化機制,能夠強有力得保證不會多次實體化的情況,即使在復雜的序列化或者反射攻擊下,列舉模式都沒有問題,
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("Geely Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = https://www.cnblogs.com/weixk/p/data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
在ObjectInputStream中,對于列舉型別,是通過列舉類直接獲得唯一的列舉常量,沒有創建新的物件,維護了列舉的單例屬性:

而對于反射,在呼叫
objectClass.getDeclaredConstructor();
時會直接報錯:
java.lang.NoSuchMethodException
原因在于Enum本身就只有一個構造器:

而如果呼叫
Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);
也會直接報錯:java.lang.IllegalArgumentException: Cannot reflectively create enum objects
如果通過jad反編譯列舉類,可以看到:1.class類為final的;2.構造器為private;3.宣告的列舉物件是static和final的;4.列舉物件在static代碼塊中實體化
所以列舉單例是最安全的單例模式
8、容器單例
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);
}
}
容器單例與享元模式相似
優點:統一管理,節省資源,相當于快取
缺點:存在執行緒安全問題
9、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();
}
}
這個單例 并不能保證整個應用全域唯一,但能保存執行緒唯一,
ThreadLocal會為每一個執行緒提供一個變數副本,本身是基于ThreaLocalMap實作的,維持了執行緒間的隔離,原理是以空間換時間的方式,會創建很多物件,在一個執行緒里會創建唯一的一個物件,在多執行緒訪問的時候,彼此不會相互影響,
三、原始碼示例
1、JDK中的Runtime:餓漢式

2、JDK中的Desktop:懶漢式+容器式+執行緒安全控制

3、spring

4、mybatis:ThreadLocal

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/8567.html
標籤:設計模式
