JAVA设计模式之单例模式



一、简介

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升
单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

二、类型

  • 饿汉式:在类加载时已经创建好对象,等待被程序使用
  • 懒汉式:在需要使用对象时才去创建该单例对象

三、饿汉模式

3.1、正常创建

  • 构造器私有(外部无法直接new对象)
  • 手动创建好一个类对象
  • 提供方法给外部获取该对象
public class Hungry {
    //构造器私有,外部无法直接new对象
    private Hungry() {}
    
    //手动创建静态单实例
    private final static Hungry HUNGRY = new Hungry();

    //给外部提供获取该单实例的方法
    public static Hungry getInstance() {
        return HUNGRY;
    }
}

缺点:可能会浪费内存空间

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];

3.2、静态内部类

public class Holder {

    private Holder() {
    }

    private static Holder getInstance() {
        return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }

}


四、懒汉模式

4.1、单线程使用

存在线程不安全的问题

public class Lazy {

    private Lazy() {}

    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            lazy = new Lazy();
        }
        return lazy;
    }   
}

如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
在这里插入图片描述
通过加锁解决
但如果直接在方法或者类上加锁,虽然解决多线程问题,但每次获取对象都要先获取锁,性能比较低

public static synchronized Lazy getInstance() {
    if (lazy == null) {
        lazy = new Lazy();
    }
    return lazy;
}
// 或者
public static Lazy getInstance() {
    synchronized(Lazy.class) {   
        if (lazy == null) {
            lazy = new Lazy();
        }
    }
    return lazy;
}

4.2、双重检测锁(DCL)

如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

  • 假设AB线程一起执行getInstance(),此时lazy为空,非空判断通过,接着AB争抢锁
  • B抢到了并锁住,再次判断lazy为空(假设别的线程比B先进入,没有这层判断会导致B又创建了对象),创建对象
  • 轮到A了,判断lazy不为空(没有这层判断会导致A又创建了对象),直接跳出,返回B创建的对象
  • 假设C进来了,非空判断过不去,直接返回B创建的对象
  • 如果没有第一层的判断会导致创建了对象后新线程又获取锁,性能降低
public class Lazy {

    private Lazy() {}
    
    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {		
            synchronized (Lazy.class) {	
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

}

解决了性能低效+并发安全的问题,但jvm创建对象的过程存在指令重排的现象

4.3、原子性操作

new对象不是原子性操作,内部执行以下

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把对象指向内存空间

但jvm有可能出现指令重排(执行132)

  • A线程执行了13,这个时候lazy的内存空间不等于null,但其实对象还未初始化
  • B线程进来了,虽然B进不去同步代码块,但同步代码块前的空判断会判断为非空
  • 此时lazy未完成构造,会直接return空对象

在这里插入图片描述

可以使用volatile关键字防止指令重排的发生

public class Lazy {

    private Lazy() {}

    //防止初始化过程指令重排
    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

}

五、反射与单例的斗争

5.1、反射破坏单例

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Lazy instance1 = Lazy.getInstance();
    Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
    //无视私有构造
    declaredConstructor.setAccessible(true);
    Lazy instance2 = declaredConstructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

在这里插入图片描述

5.2、私有构造加锁–三重检测锁

但这种只能存在先获取了单例,再通过反射创建对象,如果对象都是反射创建的,这样也无法避免

private Lazy() {
    synchronized (Lazy.class) {
        if (lazy != null) {
            throw new RuntimeException("请勿使用反射破坏单例");
        }
    }
}

在这里插入图片描述

5.3、反射创建对象

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    //Lazy instance1 = Lazy.getInstance();
    Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
    //无视私有构造
    declaredConstructor.setAccessible(true);
    Lazy instance2 = declaredConstructor.newInstance();
    Lazy instance3 = declaredConstructor.newInstance();
    System.out.println(instance2);
    System.out.println(instance3);
}

在这里插入图片描述

5.4、私有构造加标记判断

private static boolean laptoy = false;
private Lazy() {
    synchronized (Lazy.class) {
        if (laptoy == false) {
            laptoy = true;
        } else {
            throw new RuntimeException("请勿使用反射破坏单例");
        }
    }
}

在这里插入图片描述

5.5、若标记被找到

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
    //Lazy instance1 = Lazy.getInstance();
    Field laptoy = Lazy.class.getDeclaredField("laptoy");
    laptoy.setAccessible(true);
    Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
    //无视私有构造
    declaredConstructor.setAccessible(true);
    Lazy instance2 = declaredConstructor.newInstance();
    
	laptoy.set(instance2, false);
	
    Lazy instance3 = declaredConstructor.newInstance();
    System.out.println(instance2);
    System.out.println(instance3);
}

在这里插入图片描述

六、枚举(无法被反射破坏)

6.1、使用

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }

}

class Test {
    public static void main(String[] args) {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;  //相同
    }
}

6.2、验证

1、从生成的target目录发现EnumSingle源码类有私有构造
在这里插入图片描述
2、尝试用反射newInstance

class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

3、报错,没有此类型的构造器,也就是没有私有无参构造器
在这里插入图片描述
4、达不到预期,应该是抛出无法反射地创建枚举对象
在这里插入图片描述

5、反编译该类,的确有该构造,但是反射是绝对正确的
在这里插入图片描述
6、使用更专业的反编译工具,发现该类没有无参构造,但有一个有参构造
在这里插入图片描述

class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

达到预期效果,反射无法破坏单例
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Laptoy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值