Java单例模式详解

单例模式可以说是设计模式中最简单和最基础的一种设计模式了,但是想写对单例模式真的很难,下面看下单例模式的几种写法及其存在的问题分析。

饿汉式

饿汉式是最常见也是最不需要考虑太多的单例模式,因为本就是线程安全的,饿汉式也就是在类被加载的时候就创建实例对象,饿汉式的代码如下:

public class Singleton {
    private static Singleton singleton = new Singleton();

    /**
     * 私有构造函数
     */
    private Singleton() {

    }

    public static Singleton getInstance() {
        return singleton;
    }
}

优点:是线程安全的

缺点:在类被加载的时候对象就会被创建,也就是说不管是否使用到该对象此对象都会被创建,浪费内存空间,而且如果对象的创建比较复杂,耗时比较久,这也会导致整个加载时间变长

懒汉式

代码如下:

public class Singleton {
    private static Singleton singleton = null;

    /**
     * 私有构造函数
     */
    private Singleton() {

    }

    public static Singleton getInstance() {
        /*存在线程安全问题
         *当线程一执行了if (singleton == null) 没执行singleton = new Singleton();
         *此时线程二执行if (singleton == null) 这种情况就会导致线程一和线程二拿到不同的对象
         */
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:单线程下性能比较好

缺点:多线程环境下会存在线程安全问题,见上面的注释

DCL(双重检查锁)

public class Singleton {
    private static Singleton singleton = null;

    /**
     * 私有构造函数
     */
    private Singleton() {

    }

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

我最开始时认为这种形式已经是最终的形式了,首先是判断对象是否为空,如果为空,则先加锁之后再判断一次是否为空,如果还为空,则再来执行实例化对象操作,一切看起来似乎没雨任何问题,直到看过《并发编程的艺术》这一本书之后看到有问题。具体问题如下

singleton = new Singleton();可以分解为如下三行代码:

memory = allocate();     //1、分配存储空间
ctorInstance(memory);  //2、初始化对象
singleton = memory;   //3、设置singleton指向刚分配的内存地址

上面步骤3设置singleton的值和步骤2初始化对象可能会被重排序,这种重排序是真实存在的。如果3被重排序到2之前,此时其他线程刚好判断到singleton!=null,会直接使用一个没有进行初始化的对象。

知道了问题存在的原因,解决办法也很简单。有如下两种解决办法:

1、不允许2 和 3重排序

2、允许2 和 3重排序,但是不允许其他线程“看到”这个重排序

基于volatile的解决方案(即不允许2 和 3 重排序)

public class Singleton {
    private static volatile Singleton singleton = null;

    /**
     * 私有构造函数
     */
    private Singleton() {

    }

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

只需要把singleton设置为volatile,注意这个解决方案需要JDK5或者更高版本,因为从JDK5开始使用心得JSR-133内存模型规范,这个规范增强了volatile的语义。JMM针对编译器制定的volatile重排序规则表。参考《并发编程的艺术》一书。

JMM会在每个volatile写操作的前面插入一个StoreStore屏障

在每个volatile些操作的后面插入一个StoreLoad屏障

在每个volatile读操作的后面插入一个LoadLoad屏障

在每个volatile读操作的后面插入一个LoadStore屏障

从而阻止了2和3的重排序。

基于类初始化的解决方案(允许2 和 3重排序,但是不允许其他线程“看到”这个重排序)

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

public class Singleton {
    private Singleton() {
    }

    private static class SingletonHolder {
        public static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}

但是上面的单例模式都不是完美的,主要有以下两个原因。

1、反射攻击

首先我们来验证DCL的反射攻击

public class SingletonTest {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Class<Singleton> singletonClass = Singleton.class;

        //获取类的构造器
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();

        //把构造器私有权限放开
        constructor.setAccessible(true);

        //调用反射获取对象
        Singleton instance = constructor.newInstance();

        //正常的获取实例方式   正常的方式放在反射创建实例后面,这样当反射创建成功后,单例对象中的引用其实还是空的,反射攻击才能成功
        Singleton instance1 = Singleton.getInstance();
        System.out.println("instance1 = " + instance1);
        System.out.println("instance = " + instance);
    }
}

输出的结果如下,发现居然是不同的对象。

2、序列化攻击

需要序列化,需要实现接口 Serializable 

public class SingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("singleton_file"));

        //序列化写操作
        objectOutputStream.writeObject(singleton);

        //读取序列化的文件并进行反序列化操作
        File file = new File("singleton_file");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        Singleton newSingleton = (Singleton) objectInputStream.readObject();
        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
}

输出结果如下:

结果果然不一样,这种反序列化攻击其实解决方式也简单,重写反序列化时要调用的 readObject 方法即可

如下:至于为什么加一个就可以,和接口 Serializable  的作用 ,放到后续单独开一篇序列化和反序列化的文章进行说明。

private Object readResolve(){
    return instance;
}

真正安全的单例模式:枚举,枚举相关的可以看下这篇文章

public enum EnumSingleton {

    /**
     * 实例对象
     */
    INSTANCE;

    public void sayHello() {
        System.out.println("Hello World!");
    }
}

反编译看下如下:

public enum EnumSingleton {

   INSTANCE;
   // $FF: synthetic field
   private static final EnumSingleton[] $VALUES = new EnumSingleton[]{INSTANCE};


   public void sayHello() {
      System.out.println("Hello World!");
   }

}

我们试下枚举反射时会出现什么问题:

public class EnumSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<EnumSingleton> singleTonEnumClass = EnumSingleton.class;
        //获取构造函数
        Constructor<EnumSingleton> declaredConstructor = singleTonEnumClass.getDeclaredConstructor();

        //设置访问权限
        declaredConstructor.setAccessible(true);
        EnumSingleton singleTonEnum = declaredConstructor.newInstance();
        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println("instance = " + instance);
        System.out.println("singleTonEnum = " + singleTonEnum);
    }
}

结果如下:

发现抛出异常了,究竟是什么原因导致抛出异常?提示没有 com.Ycb.singleton.EnumSingleton.<init>()

根据堆栈可以看出实在Class的getConstructor0()方法跑出来的。Class的代码如下

private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                        int which) throws NoSuchMethodException
    {
        Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
        for (Constructor<T> constructor : constructors) {
            if (arrayContentsEq(parameterTypes,
                                constructor.getParameterTypes())) {
                return getReflectionFactory().copyConstructor(constructor);
            }
        }
        throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
    }

看下EnumSingleton的字节码,发现<init>方法带有两个参数,第一个参数是String,第二个参数是Int类型的,并没有无参的<init>方法

反射时获取有参的构造函数。

//获取构造函数
Constructor<EnumSingleton> declaredConstructor = singleTonEnumClass.getDeclaredConstructor(String.class, int.class);

执行结果如下:发现还是报错,但是报错变了。

提示14行报错,14行是 EnumSingleton singleTonEnum = declaredConstructor.newInstance();接着看下 Constructor 的newInstance()方法。

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

通过看 Constructor 的newInstance()方法,如果是Enum时直接抛出异常了,不允许通过反射来创建,这才是使用 enum 创建单例才可以说是真正安全的原因!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值