JUC并发编程学习笔记(十七)彻底玩转单例模式
徹底玩轉(zhuǎn)單例模式
單例中最重要的思想------->構(gòu)造器私有!
惡漢式、懶漢式(DCL懶漢式!)
惡漢式
package single;
//餓漢式單例(問題:因為一上來就把對象加載了,所以可能會導(dǎo)致浪費內(nèi)存)
public class Hungry {
    /*
    * 如果其中有大量的需要開辟的空間,如new byte[1024*1024]這些,那么一開始就會加載,而不是需要時才加載,所以非常浪費空間
    *
    * */
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    private Hungry() {
    }
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}
懶漢式
DCL懶漢式
完整的雙重檢測鎖模式的單例、懶漢式、DCL懶漢式
package single;
public class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread() + "ok");
    }
    private volatile static LazyMan lazyMan;
    //    單線程下確實ok
    public static LazyMan getInstance() {
//        加鎖、鎖整個類
//        雙重檢測鎖模式的單例、懶漢式、DCL懶漢式
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan == null) {
                    lazyMan = new LazyMan();//不是原子性操作
                }
            }
        }
        return lazyMan;
    }
    /*
     * 1、分配內(nèi)存空間
     * 2、執(zhí)行構(gòu)造方法,初始化對象
     * 3、把這個對象指向這個空間
     *
     * 期望的結(jié)果:1、2、3
     * 但是由于指令重排可能導(dǎo)致結(jié)果為1、3、2,這在cpu中是沒問題的
     * 線程A:1、3、2
     * 線程B如果在線程A執(zhí)行到3時開始執(zhí)行判斷是否為null,由于已經(jīng)占用空間了,所以會被判斷為不為空,但實際還未初始化對象,實際結(jié)果還是為null
     *
     *
     * */
    //    多線程并發(fā)測試
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}
但是有反射!只要有反射,任何的代碼都不安全,任何的私有關(guān)鍵字都是擺設(shè)
正常的單例模式:
/*
* 正常的單例模式創(chuàng)建的都為同一個對象,并且該對象全局唯一
* 只執(zhí)行一次創(chuàng)建,并且對象都是同一個
* Thread[main,5,main]ok
* true
* */
LazyMan instance1 = LazyMan.getInstance();
LazyMan instance2 = LazyMan.getInstance();
System.out.println(instance2==instance1);
反射破壞單例:
/*
* 通過反射破壞單例
* 執(zhí)行兩個創(chuàng)建,兩個不同的對象
* Thread[main,5,main]ok
  Thread[main,5,main]ok
  false
* */
LazyMan instance1 = LazyMan.getInstance();
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance2 == instance1);
怎么去解決這種破壞呢?
首先反射走了無參構(gòu)造器,我們可以在構(gòu)造器中進行加鎖判斷是否已經(jīng)存在了對象。
private LazyMan() {
    //通過構(gòu)造器來加鎖判斷防止反射破壞
    synchronized (LazyMan.class){
        if (lazyMan!=null){
            throw new RuntimeException("不要試圖使用反射破壞單例模式");
        }
    }
}
通過反射破壞單例模式
道高一尺,魔高一丈
1、通過普通的反射來破壞單例模式
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = LazyMan.getInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
解決方法:通過構(gòu)造器加鎖解決
private LazyMan() {
    //通過構(gòu)造器來加鎖判斷防止反射破壞
    synchronized (LazyMan.class){
        if (lazyMan == null){
        }else {
            throw new RuntimeException("不要試圖使用反射破壞單例模式");
        }
    }
}
2、通過反射創(chuàng)建兩個類來破壞單例模式
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1);
System.out.println(lazyMan2);
解決方法:設(shè)置一個外部私有變量,在構(gòu)造方法中通過外部私有變量來操作
//創(chuàng)建一個外部的標(biāo),用于防止通過newInstance破壞單例模式
private static boolean flg = true;
private LazyMan() {
    //通過構(gòu)造器來加鎖判斷防止反射破壞
    synchronized (LazyMan.class){
        if (flg){
            flg = false;
        }else {
            throw new RuntimeException("不要試圖使用反射破壞單例模式");
        }
    }
}
3、通過反射字段來將外部私有變量修改。
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
//通過反射修改內(nèi)部私有變量
Field flg1 = LazyMan.class.getDeclaredField("flg");
flg1.setAccessible(true);
//通過反射的newInstance創(chuàng)建的兩個對象依舊破壞了單例模式
LazyMan instance1 = declaredConstructor.newInstance();
//通過反射字段對單例模式進行破壞
flg1.set(instance1,true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance2 == instance1);
解決方法,通過枚舉類型!枚舉類型自帶單例模式,禁止反射破壞
package single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
//枚舉類
public enum EnumDemo {
    INSTANCE;
    public EnumDemo getInstance(){
        return INSTANCE;
    }
}
class EnumDemoTest{
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumDemo enumDemo1 = declaredConstructor.newInstance();
        EnumDemo enumDemo2 = declaredConstructor.newInstance();
        System.out.println(enumDemo1);
        System.out.println(enumDemo2);
    }
}
發(fā)現(xiàn)抱錯,沒有對應(yīng)的無參構(gòu)造
但是idea編譯的源碼中是由無參構(gòu)造的
idea欺騙了我們,那么編譯好的類到底有沒有無參構(gòu)造,通過javap -p反編譯源碼查看所以方法
可以看到,也有空參的構(gòu)造方法,也就意味了反編譯源碼也欺騙了你,所以我們通過更專業(yè)的工具來查看,使用jad查看。
查看當(dāng)前目錄新生成的java文件可以發(fā)現(xiàn),通過jad反編譯的源碼的構(gòu)造函數(shù)時個有參構(gòu)造函數(shù)
// 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:   EnumDemo.java
package single;
public final class EnumDemo extends Enum
{
public static EnumDemo[] values()
{
    return (EnumDemo[])$VALUES.clone();
}
public static EnumDemo valueOf(String name)
{
    return (EnumDemo)Enum.valueOf(single/EnumDemo, name);
}
private EnumDemo(String s, int i)
{
    super(s, i);
}
public EnumDemo getInstance()
{
    return INSTANCE;
}
public static final EnumDemo INSTANCE;
private static final EnumDemo $VALUES[];
static 
{
    INSTANCE = new EnumDemo("INSTANCE", 0);
    $VALUES = (new EnumDemo[] {
        INSTANCE
    });
}
}
我們嘗試在反射中加入這兩個參數(shù)類
Constructor<EnumDemo> declaredConstructor = EnumDemo.class.getDeclaredConstructor(String.class,int.class);
可以發(fā)現(xiàn),它根據(jù)我們預(yù)想的結(jié)果拋出一個異常
在newInstance方法中如果時枚舉類就會拋出這個異常,這是從反射層面限制了對枚舉類單例模式的破壞!!
總結(jié)
以上是生活随笔為你收集整理的JUC并发编程学习笔记(十七)彻底玩转单例模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 拜佛诵经后千万不要忘了做“回向”
- 下一篇: Ansible自动化部署工具-组件及语法
