十八、單例模式
程式員必會!!!
1、餓漢式
// 餓漢式單例
public class Hungry {
// 在餓漢式單例下 這些資源一起全部加載進來
// 會造成空間浪費
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() { // 構造器私有
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
餓漢式與懶漢式的本質區別:
-
餓漢式是 執行緒安全 的,在類創建的同時就已經創建好一個靜態的物件供系統使用,以后不再改變;懶漢式 如果在創建實體物件時不加上synchronized,則會導致對物件的訪問不是 執行緒安全 的,
-
從實作方式來講:懶漢式是延時加載,只有在需要的時候才創建物件;
而餓漢式在虛擬機啟動的時候就會創建,餓漢式無需關注多執行緒問題、寫法簡單明了、能用則用,但是它是加載類時創建實體、所以如果是一個 工廠模式 ,快取了很多實體、那么就得考慮效率問題,因為這個類一加載,則把所有實體不管用不用一塊創建,
2、懶漢式
雙重檢查鎖(Double Checked Locking)
在單執行緒情況下,實作單例模式是安全的,但是如果考慮多執行緒,就可能會出現問題,導致出現多個LazyMan的實體!
原因:考慮可能有兩個執行緒同時呼叫getInstance(),LazyMan就會被實體化兩次 并且被不同物件持有,完全違背單例模式的初衷,
解決方法一 加鎖 :
// 懶漢式單例-加鎖
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
加鎖雖然能解決問題,但是因為用到了synchronized,會導致很大的性能開銷,并且每次初始化時都會加鎖,性能浪費,
解決方法二、雙重檢查鎖(有缺陷)
先判斷物件是否已被初始化,再決定要不要加鎖
// 懶漢式單例-加鎖
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
}
return lazyMan;
}
}
這樣寫的話,運行順序就會變成:
- 檢查變數是否被初始化(先不去獲得鎖),如果已被初始化則立即回傳
- 獲得鎖
- 再次檢查變數是否已被初始化,如果還沒被初始化,就初始化一個物件
執行雙重檢查是因為:如果多個執行緒同時通過了第一次檢查,并且其中一個執行緒首先通過第二次檢查并實體化了物件,其余通過了第一次檢查的執行緒就不會再去實體化物件,
加雙重檢查的好處:除了初始化的時候會加鎖,后續的所有呼叫都會避免枷鎖,直接回傳,解決了性能消耗的問題!
DCL 懶漢式仍然存在缺陷!
lazyMan = new LazyMan();
實體化上述物件的程序并不是原子性操作,它可以分為三步(編譯器層面):
- 分配記憶體空間
- 初始化物件,執行構造方法
- 將物件指向剛才分配的記憶體空間
但是底層的編譯器為了性能,可能會對這三步操作進行重排序(指令重排)
執行順序可能會變成 1-3-2
假設A執行緒先通過第一次判斷,獲得鎖,并且A執行緒呼叫順序為1-3-2,執行完3之后,B執行緒剛好檢查到 lazyMan 不為空,便回傳一個未初始化完成的物件,于是產生了兩個實體!
完整的DLC懶漢式 => 對lazyMan 加上 volatile 關鍵字 禁止指令重排
package com.liu.single;
// 懶漢式單例
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName() + "LazyMan ok");
}
private volatile static LazyMan lazyMan; // volatile 禁止指令重排
public static LazyMan getInstance() {
// 雙重檢測鎖模式 懶漢式單例 DCL (Double Check Locking)
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan(); // new 關鍵字 非原子性操作 可能會出現指令重排
}
}
}
return lazyMan;
}
public static void main(String[] args) {
// 多執行緒并發
// 不安全
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
小結 : 單例模式下使用volatile,可以禁止指令重排!
? DLC + volatile 才是完整的懶漢式單例
靜態內部類的懶漢式
package com.liu.single;
// 使用靜態內部類
public class Holder {
private Holder() {
}
private static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
以上的單例都不安全 = > 因為有反射的存在,可以輕松破壞單例的安全性
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
// 懶漢式單例
public class LazyMan02 {
private static boolean liu = false; // 關鍵字可以加密 防止反射破壞
private LazyMan02() {
synchronized (LazyMan02.class) {
if (liu == false) {
liu = true;
}else {
throw new RuntimeException("不要試圖用反射破壞代碼!");
}
}
//System.out.println(Thread.currentThread().getName() + "LazyMan ok");
}
private volatile static LazyMan02 lazyMan; // volatile 禁止指令重排
public static LazyMan02 getInstance() {
// 雙重檢測鎖模式 懶漢式單例 DCL (Double Check Locking)
if(lazyMan == null){
synchronized (LazyMan.class){
if (lazyMan == null) {
lazyMan = new LazyMan02(); // new 關鍵字 非原子性操作 可能會出現指令重排
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
// 反射方法 :getDeclaredFiekd() 獲得該類中宣告的所有欄位
Field liu = LazyMan02.class.getDeclaredField("liu");
// 反射 => 可以破壞單例
//LazyMan02 instance = LazyMan02.getInstance();
Constructor<LazyMan02> declaredConstructor = LazyMan02.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan02 instance = declaredConstructor.newInstance();
liu.set(instance, false); // 反射可以破壞單例的安全性
LazyMan02 instance02 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance02);
}
}
使用列舉類可以防止反射的破壞
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Enum 列舉
* 本身也是一個類
*/
public enum EnumSingle {
INSTANCE;
private EnumSingle() {
}
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
EnumSingle instance = EnumSingle.INSTANCE;
//java.lang.NoSuchMethodException:
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
通過IDEA查看列舉類底層原始碼 => 無參構造器,使用反射之后,發現報的錯誤并不是我們預想的結果

使用無參構造的Enum:報錯 => java.lang.NoSuchMethodException
預期報錯: java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Java反編譯工具Jad 下載 :https://www.jianshu.com/p/5d8736d9a32a
使用命令javap -p EnumSingle.class進行編譯:
可以看到編譯之后的.java檔案里面也是使用無參構造!

列舉型別的最終反編譯原始碼如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package com.liu.single;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/liu/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i); // 證實列舉類使用的是有參構造,引數分別是String和int
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static void main(String args[])
throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException
{
EnumSingle instance = INSTANCE;
Constructor declaredConstructor = com/liu/single/EnumSingle.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = (EnumSingle)declaredConstructor.newInstance(new Object[0]);
System.out.println(instance);
System.out.println(instance2);
}
private static EnumSingle[] $values()
{
return (new EnumSingle[] {
INSTANCE
});
}
public static final EnumSingle INSTANCE = new EnumSingle("INSTANCE", 0);
private static final EnumSingle $VALUES[] = $values();
}
將Enum類中通過反射傳入兩個引數作為構造器的引數 : String.class int.class
package com.liu.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public enum EnumSingle {
INSTANCE;
private EnumSingle() {
}
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
EnumSingle instance = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class); // 使用String.class 和 int.class作為構造器的引數
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
其報錯結果為:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
即:無法通過反射來創建列舉類的實體 => 單例模式下使用列舉enum可以有效阻止反射,保證單例模式的安全性不被破壞!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/289101.html
標籤:其他
