0%

单例

一、含义

“单例”的书面含义是:在一个Java进程中,某个给定类的实例只允许存在一个。

二、实现方案

实现“单例”方案须满足两个核心目标:

  • 的确是“单例”
  • 线程安全

常见的单例实现方案有6种,具体可细分为3类:静态初始化锁机制,内置锁/高级锁,枚举。

接下来基于以下几个维度对上述6种单例实现方案进行评价:

  • 实现可读性,用5分制来进行评价
  • 性能
  • 是采用“懒汉模式(在获取单例实例时才生成)”还是“饿汉模式(在获取单例实例前已生成)”

2.1、静态初始化锁机制

利用静态初始化锁机制来获得“确保单例”和“线程安全”。
1、方案1

package single;

public class Singleton1 {

    private static Singleton1 instance = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return instance;
    }
}
实现可读性 性能 懒汉模式/饿汉模式
5 饿汉模式

2、方案2

package single;

public class Singleton2 {

    private static Singleton2 instance;

    static {
        instance = new Singleton2();
    }

    private Singleton2() {
    }

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

一个简单变种。

实现可读性 性能 懒汉模式/饿汉模式
5 饿汉模式

3、方案3

package single;

public class Singleton3 {

    private Singleton3() {
    }

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

    private static class SingletonHolder {
        private static final Singleton3 INSTANCE = new Singleton3();
    }
}

采用懒汉模式,代价是损失一点可读性。

实现可读性 性能 懒汉模式/饿汉模式
4 懒汉模式

2.2、内置锁/高级锁

利用“内置锁(即synchronized锁)”或者高级锁来获得“线程安全”,利用条件判断获得“确保单例”。
4、方案4

package single;

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }

        return instance;
    }
}

每次获取单例都需要申请锁和释放锁,大部分情况是不必要的,性能较差。

实现可读性 性能 懒汉模式/饿汉模式
5 懒汉模式

5、方案5

package single;

public class Singleton5 {

    private static Singleton5 instance;

    private static volatile boolean init = false;

    private Singleton5() {
    }

    public static synchronized Singleton5 getInstance() {
        if (!init) {
            synchronized (Singleton5.class) {
                if (!init) {
                    instance = new Singleton5();

                    init = true;
                }
            }
        }

        return instance;
    }
}

经典的双重检查锁定方式,“init”变量需要用volatile修饰的原因在于:否则,由于重排序/狭义可见性,外层的if (!init)语句返回false时,“instance”变量可能还未完成初始化。

实现可读性 性能 懒汉模式/饿汉模式
5 懒汉模式

在以下简单变种中,“instance”变量仍然需要用volatile修饰的原因在于:否则,由于重排序的存在,外层的if (instance == null)返回false时,“instance”变量可能还未完成初始化,只不过是由于重排序先返回了变量引用而已。

package single;

public class Singleton55 {

    private static volatile Singleton55 instance;


    private Singleton55() {
    }

    public static synchronized Singleton55 getInstance() {
        if (instance == null) {
            synchronized (Singleton55.class) {
                if (instance == null) {
                    instance = new Singleton55();
                }
            }
        }

        return instance;
    }
}

2.3、枚举

利用枚举自身的语法特性,来获得“确保单例”和“线程安全”。
6、方案6

package single;

public enum Singleton6 {
    SINGLE;

    char c;

    double d;

    Singleton6() {
        init();
    }

    public static Singleton6 getInstance() {
        return SINGLE;
    }

    private void init() {
        c = 'a';
        d = 20D;
    }
}
实现可读性 性能 懒汉模式/饿汉模式
3 饿汉模式

三、反单例攻击

“反单例攻击”含义:通过某种手段,在“单例”之外创建生成新实例。

常见的反单例攻击手段有:

  • 使用new + 构造方法创建新实例
  • 使用克隆创建新实例
  • 使用反射创建新实例
  • 使用序列化-反序列化创建新实例
  • 使用多类加载器加载创建新实例

3.1、new + 构造方法

构造方法暴露给外部,使得外部可直接使用new + 构造方法的形式创建新实例。

实验代码1:

enum C {
    SINGLE;

    char c;

    double d;

    C() {
        init();
    }

    public static C getInstance() {
        return SINGLE;
    }

    private void init() {
        c = 'a';
        d = 20D;
    }
}

class A {

    private static A instance = new A();

    public A() {
    }

    public static A getInstance() {
        return instance;
    }
}

class B {

    private static B instance;

    private static volatile boolean init = false;

    public B() {
    }

    public static synchronized B getInstance() {
        if (!init) {
            synchronized (B.class) {
                if (!init) {
                    instance = new B();

                    init = true;
                }
            }
        }

        return instance;
    }
}

public class NewConstructorAttack {
    public static void main(String[] args) {

        System.out.println(A.getInstance() == new A());

        System.out.println(B.getInstance() == new B());

        //不被允许
        //new C();
    }
}

结论

  • 由于枚举类不支持使用new + 构造方法的形式创建新实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御new + 构造方法攻击,可将构造方法设为由private修饰

3.2、克隆

类继承java.lang.Cloneable接口,并覆盖实现clone()方法,调用clone()方法可以创建生成一个新的实例。

实验代码2:

enum F implements Cloneable {
    SINGLE;

    char c;

    double d;

    F() {
        init();
    }

    public static F getInstance() {
        return SINGLE;
    }

    private void init() {
        c = 'a';
        d = 20D;
    }

// 不允许覆盖实现Object类的clone()方法
//    @Override
//    public Object clone() {
//
//    }
}

class D implements Cloneable {

    private static D instance = new D();

    private D() {
    }

    public static D getInstance() {
        return instance;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

class E implements Cloneable {

    private static E instance;

    private static volatile boolean init = false;

    private E() {
    }

    public static synchronized E getInstance() {
        if (!init) {
            synchronized (E.class) {
                if (!init) {
                    instance = new E();

                    init = true;
                }
            }
        }

        return instance;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class CloneAttack {

    public static void main(String[] args) throws CloneNotSupportedException {
        D d = D.getInstance();
        D dClone = (D) d.clone();

        System.out.println(d == dClone);

        E e = E.getInstance();
        E eClone = (E) e.clone();

        System.out.println(e == eClone);

        // F实例不能访问clone()方法
    }
}

结论

  • 枚举类不能覆盖实现clone()方法,因此方案6天然防御该形式攻击
  • 方案1-5为防御克隆攻击,可禁止继承java.lang.Cloneable接口,或者在继承的情况下,在覆盖实现的clone()方法中返回已经生成的实例,而不是重新生成,类似如下代码
    @Override
    public Object clone() throws CloneNotSupportedException {
        return instance;
    }

3.3、反射

通过反射获取Constructor对象,然后调用newInstance()方法生成实例。

实验代码3:

import java.lang.reflect.Constructor;

enum L {
    SINGLE;

    char c;

    double d;

    L() {
        init();
    }

    public static L getInstance() {
        return SINGLE;
    }

    private void init() {
        c = 'a';
        d = 20D;
    }

}

class J {

    private static J instance = new J();

    private J() {
    }

    public static J getInstance() {
        return instance;
    }
}

class K {

    private static K instance;

    private static volatile boolean init = false;

    private K() {
    }

    public static synchronized K getInstance() {
        if (!init) {
            synchronized (E.class) {
                if (!init) {
                    instance = new K();

                    init = true;
                }
            }
        }

        return instance;
    }

}

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

        Constructor jConstructor = J.class.getDeclaredConstructor();
        jConstructor.setAccessible(true);

        //返回false
        System.out.println(J.getInstance() == jConstructor.newInstance());

        // ---分割线---

        Constructor kConstructor = K.class.getDeclaredConstructor();
        kConstructor.setAccessible(true);

        //返回false
        System.out.println(K.getInstance() == kConstructor.newInstance());

        // ---分割线---

        // 枚举类的默认构造方法有“String和int”两个参数
        Constructor lConstructor = L.class.getDeclaredConstructor(String.class, int.class);
        lConstructor.setAccessible(true);

        System.out.println(L.getInstance() == lConstructor.newInstance("AnotherInstance", 1000));
    }
}

结论

  • 对于枚举类,在获得其Constructor对象,然后调用newInstance()方法时会直接抛出java.lang.IllegalArgumentException异常,查看方法实现源码可知“枚举类直接被限定不能调用该方法”,因此方案6天然防御该形式攻击
  • 方案1-5为防御反射攻击,对于“饿汉方案”,可在构造方法中增加“重复生成则抛出异常”的判断逻辑,类似如下代码;而对于“懒汉方案”,则无能为力
    if (instance != null) {
        throw new RuntimeException("the instance exists");
    }

3.4、序列化-反序列化

序列化存在多种形式,这里讨论常见的JDK序列化和JSON序列化(以Fastjson序列化为例)。

实验代码4:

import com.alibaba.fastjson.JSON;

import java.io.*;
import java.lang.reflect.InvocationTargetException;

enum I implements Serializable {
    SINGLE;

    char c;

    double d;

    I() {
        init();
    }

    public static I getInstance() {
        return SINGLE;
    }

    private void init() {
        c = 'a';
        d = 20D;
    }

}

class G implements Serializable {

    private static G instance = new G();

    private G() {
    }

    public static G getInstance() {
        return instance;
    }

}

class H implements Serializable {

    private static H instance;

    private static volatile boolean init = false;

    private H() {
    }

    public static synchronized H getInstance() {
        if (!init) {
            synchronized (E.class) {
                if (!init) {
                    instance = new H();

                    init = true;
                }
            }
        }

        return instance;
    }
}

public class DeserializationAttack {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        jdkDeserializationAttack();

        fastjsonDeserializationAttack();

    }

    private static void jdkDeserializationAttack() {

        G g = G.getInstance();

        G anotherG = (G) serializeAndDeserializeObjectJDK(g);

        //返回false
        System.out.println(g == anotherG);

        // ---分割线---

        H h = H.getInstance();

        H anotherH = (H) serializeAndDeserializeObjectJDK(h);

        //返回false
        System.out.println(h == anotherH);

        // ---分割线---
        I i = I.getInstance();

        I anotherI = (I) serializeAndDeserializeObjectJDK(i);

        //返回true
        System.out.println(i == anotherI);
    }

    private static Object serializeAndDeserializeObjectJDK(Serializable serializable) {
        try {
            ByteArrayOutputStream oo = new ByteArrayOutputStream();

            ObjectOutputStream oos = new ObjectOutputStream(oo);

            oos.writeObject(serializable);


            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(oo.toByteArray()));

            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();

            return null;
        }
    }

    private static void fastjsonDeserializationAttack() {
        G g = G.getInstance();

        G anotherG = (G) serializeAndDeserializeObjectFastjson(g, G.class);

        //返回false
        System.out.println(g == anotherG);

        // ---分割线---

        H h = H.getInstance();

        H anotherH = (H) serializeAndDeserializeObjectFastjson(h, H.class);

        //返回false
        System.out.println(h == anotherH);

        // ---分割线---
        I i = I.getInstance();

        I anotherI = (I) serializeAndDeserializeObjectFastjson(i, I.class);

        //返回true
        System.out.println(i == anotherI);
    }

    private static Object serializeAndDeserializeObjectFastjson(Object obj, Class clz) {
        try {
            String jsonStr = JSON.toJSONString(obj);

            return JSON.parseObject(jsonStr, clz);
        } catch (Exception e) {
            e.printStackTrace();

            return null;
        }
    }

}

3.4.1、JDK序列化

类继承java.lang.Serializable接口,通过对一个实例序列化和反序列化,得到一个新的实例。

结论

  • JDK序列化/反序列化对枚举类特殊处理,经过序列化-反序列化过程,不会生成一个新的实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御JDK序列化攻击,可禁止继承java.io.Serializable接口;或者在继承的情况下,覆盖实现readResolve()方法,在该方法中直接返回已经生成的实例。另外需要特别注意的一点是,JDK的反序列化过程无需调用构造方法,因此,不能采用“Fastjson序列化攻击防御”中的“排除定义无参构造方法”方案

3.4.2、Fastjson序列化

通过对一个实例进行Fastjson序列化和反序列化,得到一个新的实例。

结论

  • Fastjson序列化/反序列化对枚举类特殊处理,经过序列化-反序列化过程,不会生成一个新的实例,因此方案6天然防御该形式攻击
  • 方案1-5为防御Fastjson序列化攻击,可采用“排除定义无参构造方法(比如显式定义一个有参构造方法)”方案,本质原因在于Fastjson反序列化时需要调用无参构造方法

3.5、多类加载器加载

多个类加载器加载生成多个不同的Class对象,再生成多个不同的实例对象。

实验代码5:

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;


public class MultiClassLoaderAttack {

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

        /**
         * 注意先把类路径下的single/Singleton1.class,single/Singleton5.class,single/Singleton6.class文件删除,否则会被优先加载
         */
        ClassLoader loader1 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/ca/").toURI().toURL()});
        ClassLoader loader2 = new URLClassLoader(new URL[]{new File("/home/dslztx/Desktop/cb/").toURI().toURL()});


        Class m01 = loader1.loadClass("single.Singleton1");
        Class m02 = loader2.loadClass("single.Singleton1");
        comp(m01, m02);

        Class n01 = loader1.loadClass("single.Singleton5");
        Class n02 = loader2.loadClass("single.Singleton5");
        comp(n01, n02);

        Class o01 = loader1.loadClass("single.Singleton6");
        Class o02 = loader2.loadClass("single.Singleton6");
        comp(o01, o02);
    }

    private static void comp(Class a, Class b) throws Exception {
        // 以下结果都为false,因为同一个类被不同类加载器加载得到的Class对象不同

        System.out.println(a == b);

        Method methodA = a.getMethod("getInstance");
        Method methodB = b.getMethod("getInstance");

        System.out.println(methodA == methodB);

        Object objA = methodA.invoke(null);
        Object objB = methodB.invoke(null);

        System.out.println(objA == objB);

        // 以下结果都为true,因为在一个Class对象范畴中,存在“单例”语义,Class对象也只有一个
        System.out.println(methodA.invoke(null) == objA);
        System.out.println(methodB.invoke(null) == objB);

        System.out.println(objA.getClass() == a);
        System.out.println(objB.getClass() == b);
    }

}

结论

  • 所有方案都不能避免该形式攻击

四、狭义单例和广义单例

上面所说的单例是“狭义单例”,我们日常所说的单例实际上是“广义单例”,两者的区别在于:

  • 狭义单例,客观确保单例,防止恶意攻击
  • 广义单例,主观确保单例,不考虑恶意攻击,比如“使用Spring框架中的Bean单例模式,实际上Bean对应类的构造方法是public的”

五、“单例”与“只执行一次”

“单例”与“只执行一次”的关系:实现“单例”必然涉及到“只执行一次”,但是“只执行一次”不必然是为了实现“单例”。比如如下代码:

public class KafkaProducerFactory {
    private static void init() {
        if (!init) {
            synchronized (KafkaProducerFactory.class) {
                if (!init) {
                    try {

                        Configuration configuration = ConfigLoadAssist.propConfig(CONFIG_FILE);

                        // doSomething1()

                        // doSomething2()

                        // doSomething3()
                    } catch (Exception e) {
                        logger.error("", e);

                        throw new RuntimeException(e);
                    } finally {
                        init = true;
                    }
                }
            }
        }
    }
}

使用哪种“只执行一次”方案来实现“单例”或者用于其他用途,具体情况具体分析。


参考文献

[1]https://cloud.tencent.com/developer/article/1446979
[2]http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
[3]https://juejin.im/post/5b50b0dd6fb9a04f932ff53f
[4]http://www.hollischuang.com/archives/2498
[5]http://ifeve.com/from-singleton-happens-before/
[6]http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[7]https://www.hollischuang.com/archives/1144
[8]https://www.hollischuang.com/archives/205
[9]https://juejin.im/post/6844903753540173831
[10]https://www.cnblogs.com/giserxiaoliang/p/4922879.html

您的支持将鼓励我继续分享!