Java深入理解单例模式

单例模式

单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

注意:

  • 单例类只能有一个实例
  • 单例类必须自己创建自己的唯一实例
  • 单例类必须给所有其他对象提供这一实例

单例模式中最重要的思想:构造器私有(private),一旦构造器私有,就无法用new关键字去创建对象了。

接下来,介绍几种单例模式的写法

1.   饿汉式

package com.singleton;

//饿汉式单例
public class Hungry {

    private byte[] data= new byte[1024*1024];

    private Hungry(){}

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

饿汉式有什么问题呢?

正如代码中所示,饿汉式会浪费内存,我们在类中定义了一个数组data,当使用饿汉式单例时,会将这些数组对象都加载进入内存,但是对象空间都没有使用。造成空间浪费。饿汉式的优点是:没有线程安全的问题

 

2.  懒汉式

package com.singleton;

public class Lazy {

    private Lazy(){}

    private static Lazy instance;

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

懒汉式就是在用到实例的时候才去创建,并且创建的时候进行检查,如果没有实例化则创建新对象,否则直接返回对象,不再创建新对象。

懒汉式在单线程下,运行很好;但是在多线程并发下,会出现问题

2.1  多线程测试懒汉式

package com.singleton;

public class Lazy {

    private Lazy(){
        System.out.println(Thread.currentThread().getName()+" start...");
    }

    private static Lazy instance;

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

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                instance.getInstance();
            }).start();
        }
    }
}

多次运行输出结果:

3.   双重检测锁模式(DCL)

package com.singleton;

public class Lazy {

    private Lazy(){
        System.out.println(Thread.currentThread().getName()+" start...");
    }

    private volatile static Lazy instance;

    public static Lazy getInstance(){
        //加锁之前,可能会被多个线程拿到,所以判断两次
        if(instance == null){//1
            synchronized (Lazy.class){
                if(instance == null){//2
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                instance.getInstance();
            }).start();
        }
    }
}

判断两次解释?

假如A,B两个线程同时执行到代码1处,然后两个线程会竞争锁。假如A线程获得了锁,继续执行,直到创建一个Lazy实例对象,然后A线程释放锁。接着B线程获取锁,继续执行,在代码2处再次检查instance是否为null。由于synchronized可以保证线程可见性,所以不会再重新创建一个新的实例对象。但是,如果没有代码2的判断,线程B就会创建另一个Lazy对象,破坏单例模式。

注意:instance变量 要加上 volatile 关键字,那么为什么需要 volatile 关键字呢?

由于代码 instance=new Lazy();不是一个原子性操作,这行代码需要三个步骤:

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

由于Java虚拟机中的 指令重排 可能导致上面三个步骤并不是顺序执行,假设线程A执行顺序为132,当执行到3时,表示已经占据了内存空间,只是还没有进行初始化;如果此时线程B执行该方法,就会发现instance不为空,从而返回instance,但是实际上instance并没有初始化。

volatile 关键字有两个语义:一是保证变量的可见性,二是禁止指令重排。在这里,可见性由synchronized就可以保证,同时synchronized保证了原子性,因此DCL中volatile 的作用是禁止指令重排。

4.  静态内部类实现单例模式

package com.singleton;

//静态内部内实现
public class Single {

    private Single(){}

    public static Single getInstance(){
        return InnerClass.HOLDER;
    }

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

静态内部类的方式在效果上类似DCL,但实现更简单。但是只适用于静态域的情况,而DCL适用范围更大。

5.  枚举

public enum  EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

枚举较为少见,它支持序列化,并且绝对防止多次实例化。

 

6.  扩展

问题:DCL真的能保证单例模式安全吗?

众所周知,在java中有一个bug般的存在:反射。由于反射存在,任何private都是浮云,接下来使用反射机制破坏DCL。

6.1  反射破坏DCL

对DCL进行测试,修改main方法

package com.singleton;

import java.lang.reflect.Constructor;

public class Lazy {

    private Lazy(){}

    private static Lazy instance;

    public static Lazy getInstance(){
        //加锁之前,可能会被多个线程拿到,所以判断两次
        if(instance == null){
            synchronized (Lazy.class){
                if(instance == null){
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }


    public static void main(String[] args) throws Exception {
        //正常情况下创建对象
        Lazy instance1 = Lazy.getInstance();

        //反射创建对象  获取其空参构造器
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
        //设置权限,无视私有(private)关键字
        constructor.setAccessible(true);
        //通过构造器创建对象
        Lazy instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1==instance2);
    }
}

输出结果:

=======================================我是分割线====================================

我们发现程序最终创建了两个实例对象,可以得到结论,反射可以破坏单例。那么我们在构造器中再加一个锁能不能解决呢?试一下,修改构造器,其他代码不变

    private Lazy(){
        synchronized (Lazy.class){
            if(instance != null){
                throw new RuntimeException("不要试图使用反射搞破坏!");
            }
        }
    }

输出结果:

=======================================我是分割线====================================

从上面结果可以看出,我们好像成功了,别急修改一下main方法,在测试一次

public static void main(String[] args) throws Exception {

        //反射创建对象  1.获取其 空参构造器
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
        //设置权限,无视私有(private)关键字
        constructor.setAccessible(true);
        //通过构造器创建对象
        Lazy instance1 = constructor.newInstance();
        Lazy instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1==instance2);
}

输出结果:

运行发现,没报错???

结论:在之前的代码中,我们是通过  Lazy instance1 = Lazy.getInstance() 创建了对象,所以才会使得instance不为空,而这里我们的两个对象都是使用反射创建的,结果就没报错了。

=======================================我是分割线====================================

我们再加入一个标志位能不能解决呢?当反射创建一个对象的时候,我们修改标志位,那么就不会再次创建。在上面的基础上,我们添加一个标志位,并且修改构造器

package com.singleton;

import java.lang.reflect.Constructor;

public class Lazy {

    private static boolean flag = false;

    private Lazy(){
        synchronized (Lazy.class){
            if(flag == false){
                flag = true;
            }else{
                throw new RuntimeException("不要试图使用反射搞破坏!");
            }
        }
    }

    private static Lazy instance;

    public static Lazy getInstance(){
        //加锁之前,可能会被多个线程拿到,所以判断两次
        if(instance == null){
            synchronized (Lazy.class){
                if(instance == null){
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }


    public static void main(String[] args) throws Exception {

        //反射创建对象  1.获取其 空参构造器
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
        //设置权限,无视私有(private)关键字
        constructor.setAccessible(true);
        //通过构造器创建对象
        Lazy instance1 = constructor.newInstance();
        Lazy instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1==instance2);
    }
}

上面代码,除了新加一个flag并且修改构造器,其他什么都没有变!!

输出结果:

=======================================我是分割线====================================

通过标志位进行控制,在不使用反编译的情况下,是找不到我们定义的标志位的,并且标志位如果再进行一些加密处理,会使得反射变得更加安全。

既然要测试,那就测试到底!!!反射既然能得到构造器,那使用反射得到字段还不是一样,创建一个对象之后,使用反射修改标志位,然后创建另一个对象。修改main方法,继续测试:

public static void main(String[] args) throws Exception {

        //反射获取标志位字段
        Field flag = Lazy.class.getDeclaredField("flag");
        //破坏其私有权限
        flag.setAccessible(true);

        //反射创建对象  1.获取其 空参构造器
        Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
        //设置权限,无视私有(private)关键字
        constructor.setAccessible(true);
        //通过构造器创建对象
        Lazy instance1 = constructor.newInstance();
        //创建一个对象后,更改标志位,重新设置false
        flag.set(instance1,false);
        Lazy instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance1==instance2);
}

输出结果:

单例再次被破坏!!!

这里可能有人问,若是不知道字段名呢,这里获取字段是直接传入一个参数flag,那如果不知道怎么办?

//反射获取标志位字段
Field[] flag = Lazy.class.getDeclaredFields();    
for (Field field : flag) {
    System.out.println(field);
}

输出结果:

是不是可以得到了呢

结论:无限套娃....

=======================================我是分割线====================================

那到底如何解决呢?我们先查看newInstance方法的源代码:

 

在上图的源码中,我们发现:如果类型是枚举类型,那么就不能反射破坏枚举,接下来,我们测试反射到底能不能破坏枚举。

6.2  反射和枚举

枚举实现单例在上面已经写过了,我们就使用上面代码进行测试

package com.singleton;

import java.lang.reflect.Constructor;

public enum  EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        //得到其无参构造
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
        //破坏其权限
        constructor.setAccessible(true);
        //得到对象
        EnumSingle instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

上述代码为:创建了一个测试类Test,并且使用反射获取枚举对象,看看是否能成功!!

输出结果:

从这里可以看出,结果抛出异常,但是这个异常是NoSuchMethodException,意思是没有空参的构造方法???是不是很奇怪,那么我们来查看一下编译后的class文件:

然而,class文件中明明存在空参构造方法呀!是不是IDEA的原因呢?反编译在试一下:

反编译之后,发现仍然存在空参构造方法,那为什么会报那样的错误呢?接下来,我们使用一个更加强大的反编译工具jad进行反编译试一下:

去查看生成的这个java文件:

查看这个文件,发现确实没有空参的构造方法,而是两个参数的构造方法,接下来,我们更改之前的测试类,通过有参构造器获取对象

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        //得到其无参构造
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        //破坏其权限
        constructor.setAccessible(true);
        //得到对象
        EnumSingle instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

输出结果:

至此,我们得到了期望的异常,与newInstance方法中描述的一致。也即不能使用反射创建枚举对象。

结论:反射不能破坏枚举的单例模式,也对应了上面枚举中的那句话  枚举绝对防止多次实例化。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值