可能是最全的Java单例模式讨论

单例模式
最简单但是也挺困难的。

要保证在一个JVM中只能存在一个实例,要考虑到如下的情况:

  • Java能够使用那些方式构建对象
  • Java在创建对象时多线程并发情况下是否仍然只能创建一个实例

Java创建对象的方法:

  • new 最常用的,直接使用构造器创建。 每new一次都会产生新的实例。所以单例中应该只new一次,当再想用对象时都返回该对象的值
  • Class.newInstance() 该方法会调用public 的无参构造器。
    为了防止这个方式创建,只要把构造器设置为private的就可以了。这是如果再用这个方法创建会报错.同时私有构造器也可以解决四处new的问题。
  • 反射
    Constructor ctt = c.getDeclaredConstructor();
    ctt.setAccessible(true);
    T t1 = ctt.newInstance();
    这样私有构造器也不行了。解决的办法是使用抽象类,这样就会抛出异常了,不能创建了。或者在构造器中加入判断如果是第二次构建就抛出异常。
  • clone
    这个主要由clone()方法的具体行为决定的。如果没有实现Cloneable接口是不用管这个问题的。
  • 反序列化
    反序列化的时候也会打破单例,解决的方式是写一个readResolve。这个方法的规则是在反序列化的时候勇气返回值来代替反序列化的返回值
    还有一个更简单的办法是不要实现Serializable接口,这样序列化的时候就会报错了

先写个验证工具,来验证这个类是否是单例的

public class SingletonTester {
    public static <T> void checkClassNewInstance(Class<T> c){

        try {
            T t1 = c.newInstance();
            T t2 = c.newInstance();
            if(t1 != t2){
                System.out.println("Class.newInstance校验失败,可以创建两个实例");
            }else{
                System.out.println("Class.newInstance校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用Class.newInstance创建,因此Class.newInstance校验通过");
        } 
    }

    public static <T> void checkContructorInstance(Class<T> c){
        try {
            Constructor<T> ctt = c.getDeclaredConstructor();
            ctt.setAccessible(true);
            T t1 = ctt.newInstance();
            T t2 = ctt.newInstance();
            if(t1 != t2){
                System.out.println("ContructorInstance校验失败,可以创建两个实例");
            }else{
                System.out.println("ContructorInstance校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用反射方式创建,因此ContructorInstance校验通过");
        } 
    }

    public static <T> void testSerializable(T t1){
        File objectF = new File("/object");  
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new FileOutputStream(objectF));
            out.writeObject(t1);
            out.flush();
            out.close();
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(objectF));  
            T t2 = (T) in.readObject();  
            in.close();

            if(t1 != t2){
                System.out.println("Serializable校验失败,可以创建两个实例");
            }else{
                System.out.println("Serializable校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用反序列化方式创建,因此Serializable校验通过");
        } 

    }

    public static void main(String[] args) {
        checkClassNewInstance(Singleton3.class);
        checkContructorInstance(Singleton3.class);
        testSerializable(Singleton3.getInstance());

    }
}

这个工具验证了Class.newInstance攻击,反射攻击,反序列化攻击,能够屏蔽着三种攻击的才是好的单例。

单例1

public class Singleton1{
    private Singleton1() {
    }

    private static Singleton1 instance;

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

最普通懒汉模式的单例, 私有构造器,静态方法获取实例,获取的时候先判空。
测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过

这个类因为不能被序列化,因此不会受到反序列化攻击
因为私有构造器避免了Class.newInstance
但是会被反射攻击
另外其不是线程安全的

单例2

public class Singleton2 {
    private static Singleton2 sington = new Singleton2();

    private Singleton2(){};

    public static Singleton2 getInstance(){
        return sington;
    }
}

来个典型的饿汉模式的
测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过

同样不会有反序列化及Class.newInstance的问题。
并且没有并发的问题。
不过其会在不同的时候也初始化一个实例出来。个人感觉实际上影响不大

单例3

上面的都会有反射攻击的问题。来解决它。

public class Singleton3 {
    private static Singleton3 sington = new Singleton3();
    private static int COUNT = 0;
    private Singleton3(){
        if(++COUNT > 1){  
            throw new RuntimeException("can not be construt more than once");  
        } 
    };

    public static Singleton3 getInstance(){
        return sington;
    }
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
不能用反序列化方式创建,因此Serializable校验通过

通过加入计数器来解决,这样虽然解决了反射攻击,但是却不是线程安全的,另外引入了新的变量也不优雅。下面换个方式:

单例4

public abstract class Singleton4 {
    private static class SingletonHolder{
        private static final Singleton4 INSTANCE = new Singleton4() {
        };
    }

    private Singleton4(){};

    public static Singleton4 getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

这个推荐使用

  • 用抽象类解决了反射攻击
  • 用类加载的线程安全性解决了并发
  • 用内部类实现了lazyloader的目的
  • 没有实现clone
  • 没有实现Serializable接口不会有反序列化的问题

单例5

下面说下不用内部类的懒汉模式

public class Singleton5 {
    private static Singleton5 sington = null;

    private Singleton5(){};

    public static Singleton5 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton5.class) {
                if(sington == null){ // 2
                    sington = new Singleton5();
                }
            }
        }
        return sington;
    }
}

如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。
此外其会被反射攻击

单例6

上面的会有线程安全问题,是由于JVM的重排序机制引起的:
重排序:
JVM在编译的时候会保证单线程模式下的结果是正确的,但是其中代码的顺序可能会进行重排序,或者乱序,主要是为了更好的利用多cpu资源(乱序), 以及更好的利用寄存器,。
比如1 a = 1; b = 2; a=3;三个语句,如果b执行的时候可能会占用a的寄存器位置,JVM可能会把a=3语句提到b=2前面,减少寄存器置换次数。
比如上面的 instance = new Singleton5()这部分代码的伪字节码为:
1. memory = allocate() // 分配内存
2. init(memory) // 初始化对象
3. instance = memory // 实例指向刚才初始化的内存地址。
4. 第一次访问instance
在JVM的时候有可能2.3的位置进行了重新排序,因为JVM只保证构造器执行完之后的结果是正确的,但是执行顺序可能会有变化。 这个时候并发调用getInstance的时候就有可能出现如下的情况:

时间 线程A 线程B
t1 A1:分配对象的内存空间
t2 A3:设置instance指向内存空间
t3 B://1 处判断instance是否为空
t4 B:由于instance不为null,线程B将返回instance引用的对象
t5 B:instance没有经过初始化,可能会有未知问题
t6 A2:初始化对象
t7 A:这是对象才是被初始化的

为了解决这个问题,我们可以从两个方向考虑:制止重排序,或者使重排序对其他线程不可见。

制止重排序的方式单例
使用JDK1.5之后提供的volatile关键字。这个关键字的意义在于保证变量的可见性。保证变量的改变肯定会回写主内存,并且关闭java -server模式下的一些优化,比如重排序:

public abstract class Singleton6 {
    private static volatile Singleton6 sington = null;

    private Singleton6(){};

    public static Singleton6 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton6.class) {
                if(sington == null){ // 2
                    sington =  new Singleton6(){};;
                }
            }
        }
        return sington;
    }
}

还可以,但是代码有些长,不如Singleton4

单例7

使重排序对其他线程不可见的单例

public abstract class Singleton7 {
    private static Singleton7 sington = null;

    private Singleton7(){};

    public static Singleton7 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton7.class) {
                if(sington == null){ // 2
                    Singleton7 temp = new Singleton7(){};
                    sington = temp;
                }
            }
        }
        return sington;
    }
}

另外单例4页是这样的,重排序对其他的线程是不可见的

单例8

如果有必要序列化,那么就需要实现Serializable接口,下面说下这种情况如何解决反序列化攻击的问题

public abstract class Singleton8 implements Serializable{
    private static class SingletonHolder{
        private static final Singleton8 INSTANCE = new Singleton8() {
        };
    }

    private Singleton8(){};

    public static Singleton8 getInstance(){
        return SingletonHolder.INSTANCE;
    }

    public Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过

这个主要在于方法readResolve, 其返回结果会用来代替反序列化的结果

单例9

枚举单例,effectiveJava中推荐的
最后一个了。就是使用枚举单例了。可以看一下,是极好用的

public enum SingleEnum {
    INSTANCE;  
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过

它也成功的避免了各种可能存在的问题:

  • 用抽象类解决了反射攻击
  • 用类加载的线程安全性解决了并发
    其类加载部分的代码:
public abstract class Enum{
    private Enum{}
    private static Enum INSTANCE = null;
    static{
        INSTANCE = new Enum(){};
    }
}
  • 用静态方法初始化保证了线程安全,会在类加载的时候初始化
  • 没有实现clone
  • 不会有反序列化的问题, 这个使用javap 仍然没有看到类似于readObject的源代码,应该是jdk内部生成字节码的时候做了某些操作。

好了,综上,尽量用枚举单例,或者是Holder单例吧

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。