单例模式详解

饿汉单例模式

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private final static ScpD scpD=new ScpD();

    public static ScpD getScpD() {
        return scpD;
    }
}

类一经创建就会给对象分配内存,这种方式会造成不必要的内存浪费

懒汉单例模式

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private static ScpD scpD;
    
    private ScpD(){
        
    }

    public static ScpD getScpD() {
        if(scpD==null) scpD=new ScpD();
        return scpD;
    }
}

这里构造方法被定义为私有,只有在调用的时候才会给对象分配内存,但是该方式在多线程下有问题

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private static ScpD scpD;

    private final String DId;
    private ScpD(){
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
    public static ScpD getScpD() {
        if(scpD==null) scpD=new ScpD();
        return scpD;
    }
    public void show(){
        System.out.println(this.DId);
    }

}

package com.std.www;

import com.std.www.singletonmode.ScpD;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        for(int i=0;i<10;i++){
            new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
        }
    }
}

可以发现创建了10个对象,并不是单例模式,对此我们需要给该类上一把锁

 

package com.std.www.singletonmode;

import java.util.UUID;

public class ScpD {
    private volatile static ScpD scpD;//volatile关键字防止指令重排

    private final String DId;
    private ScpD(){
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
    public static ScpD getScpD() {
        if(scpD==null)//双重检测锁模式DCL懒汉式单例,synchronized用于同步多线程,表示该代码块在该类中一次只能由一个线程执行,volatile防止指令重排
            synchronized (ScpD.class) {
                //正常指令顺序:1.分配内存空间2.初始化对象3.对象引用该内存
                //多线程可能导致出现空指针异常即改变了原有的执行顺序,比如先引用该内存在对象初始化之前
                if (scpD == null) {
                    scpD = new ScpD();
                }
            }
        return scpD;
    }
    public void show(){
        System.out.println(this.DId);
    }

}

package com.std.www;

import com.std.www.singletonmode.ScpD;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        for(int i=0;i<10;i++){
            new Thread(()->{ScpD scpD=ScpD.getScpD();scpD.show();}).start();
        }
    }
}

上面虽然解决的多线程问题, 但是依然不安全,看下面的例子

package com.std.www;

import com.std.www.singletonmode.ScpD;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
        ScpD scpD=ScpD.getScpD();
        //反射获取构造器
        Constructor<? extends ScpD> scpDecCon = scpD.getClass().getDeclaredConstructor();
        //设置私有构造器可访问
        scpDecCon.setAccessible(true);
        //通过构造器新建对象
        ScpD scpD1=scpDecCon.newInstance();
        scpD.show();
        scpD1.show();
    }
}

 这里发现通过反射依然可以破坏掉单例模式的安全性,下面的是解决办法,修改构造器再次判断

synchronized (ScpD.class) {//三重检测
    if (scpD != null) {
        throw new RuntimeException("检测到有反射企图破坏单例模式");
    }
    this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
}

不过这里依然有一个小问题,那就是如果一开始就没有使用单例创建对象的话

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
    //反射获取构造器
    Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
    //设置私有构造器可访问
    scpDecCon.setAccessible(true);
    //通过构造器新建对象
    ScpD scpD1=scpDecCon.newInstance();
    ScpD scpD2=scpDecCon.newInstance();
    scpD1.show();
    scpD2.show();
}

对此我们可以设立一个判断依据

private ScpD() {
    private static boolean flag=false;
    synchronized (ScpD.class) {//三重检测
        if (flag) {
            throw new RuntimeException("检测到有反射企图破坏单例模式");
        }
        else flag=true;
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
}

 不过这里依然有破解办法

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
   //反射获取私有属性字段
    Field flag=ScpD.class.getDeclaredField("flag");
    //设置字段可访问
    flag.setAccessible(true);
    //反射获取构造器
    Constructor<? extends ScpD> scpDecCon = ScpD.class.getDeclaredConstructor();
    //设置私有构造器可访问
    scpDecCon.setAccessible(true);
    //通过构造器新建对象
    //flag:false
    ScpD scpD1=scpDecCon.newInstance();
    //flag:true
    //设置字段属性
    flag.set(flag,false);
    //flag:false
    ScpD scpD2=scpDecCon.newInstance();
    //flag:true
    scpD1.show();
    scpD2.show();
}

这里我们可以搞一个小小的优化

private static enum Flag{
    ISNULL,
    NOTNULL
}
private static Flag flag=Flag.ISNULL;
private ScpD() {
    synchronized (ScpD.class) {//三重检测
        if (flag==Flag.ISNULL) {
            throw new RuntimeException("检测到有反射企图破坏单例模式");
        }
        else flag=Flag.NOTNULL;
        this.DId = "D级人员:编号" + UUID.randomUUID().toString().substring(0, 4);
    }
}

这样要想改变字段的值必须先获取得到该私有的枚举类,然后通过分析得到枚举类中的值,虽然也能通过反射改变但是要比前者复杂不少

通过源码我们可以发现

反射是无法构造一个枚举类型的对象,因此如果可以选择枚举类型作为单例模式是最为安全的

由此可得单例模式的安全性受诸多因数影响

JDK中单例模式的应用

Runtime类采用饿汉式单例模式

 

posted @ 2023-09-18 21:50  突破铁皮  阅读(16)  评论(0)    收藏  举报