估计90%的人写的单例类都有点问题吧

193 阅读12分钟

前言

写出来一个完善的单例类并不是一件特别容易的事情,大部分业务代码中的单例类会有些问题的

单例是什么

保证一个类只有一个实例,并且提供一个访问该唯一实例的全局访问点

通过这个定义,大概可以拆解成3个重要的特征:

  1. 单例类有且只有一个实例化对象
  2. 单例类唯一的实例化对象必须类内部创建
  3. 单例类创建好的实例必须提供一个全局的访问

后面我们再讲这3个特征的实现方式,那为什么要有单例这种模式呢,也就是说单例的使用场景是什么呢?

单例的使用场景

  • 统计网站的访问数量:只有1个实例统计作为收口,统计访问次数
  • 应用程序的日志记录:只有1个单例追加日志;如果1次请求中有10处记录日志的,总不能每记一条日志就实例化一次吧?这样频繁创建实例销毁实例太浪费资源了
  • 数据库线程池单例模式:主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的
  • 多线程线程池:控制线程池资源的情况下,方便线程池对线程资源的控制

总而言之,一般可以分为两种:

  1. 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如应用日志文件
  2. 控制资源的情况下,方便资源之间的互相通信。如线程池等

单例的实现方式

单例一般有两种实现方式:

  • 当我需要用的时候我再创建,这种叫做懒汉模式
  • 我一开始就创建好,也不管后面用不用,等真正用的时候直接取就行,这种叫做饿汉模式(饿怕了先做好饭,等想吃的时候再吃)

这两种方式实现的共同点都需要注意:

  • 将构造函数设置为私有(private) , 防止其他对象通过单例类的new创建对象
  • 设置一个私有的静态属性,用于保存创建好的单例对象
  • 提供一个静态方法,返回创建好的单例对象

饿汉模式

单例的饿汉模式示例代码

public class HungryManSingleton {
    //一上来就创建一个唯一不可变的对象
    private static final HungryManSingleton instance = new HungryManSingleton();

    private HungryManSingleton() {
        System.out.println(">>> 进入【HungryManSingleton】构造函数方法");
    }

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

定义静态属性instance,在类初始化阶段就实例化出来1个对象,然后通过提供getInstance()静态方法返回单例对象

类只会被初始化一次,因此只有创建出来1个单例对象

测试代码

class SingletonTest {
    @Test
    public void test() {
        HungryManSingleton singleton01 = HungryManSingleton.getInstance();
        HungryManSingleton singleton02 = HungryManSingleton.getInstance();
        System.out.println(singleton01);
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【HungryManSingleton】构造函数方法
com.xxx.HungryManSingleton@7221539
com.xxx.HungryManSingleton@7221539

优点&缺点

优点:

  • 线程安全
  • 不用加锁,执行效率比较高

缺点:

  • 浪费内存:创建后可能不使用,造成内存浪费

懒汉模式

示例代码

public class LazySingleton {
    private static LazySingleton instance = null;

    private LazySingleton() {
        System.out.println(String.format(">>> 进入【%s】构造函数方法", getClass().getSimpleName()));
    }

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

测试代码

class SingletonTest {
    @Test
    public void test() {
        LazySingleton singleton01 = LazySingleton.getInstance();
        LazySingleton singleton02 = LazySingleton.getInstance();
        System.out.println(singleton01);
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【LazySingleton】构造函数方法
com.xxx@26101efc
com.xxx@26101efc

通过输出结果可以看出两次获取的实例是同一个。 上面懒汉模式的实现代码是非线程安全的 当线程a, 线程b 同时获取实例对象时,因为 instance都是 null, 导致各自创建了实例化对象(object1object2),优化方案也比较简单,只需要判断instance时加锁控制下。

优点&缺点

优点:

  • 延迟加载,节省内存

缺点:

  • 多线程下容易引起线程安全的问题

懒汉模式:synchronized

示例代码

public class LazySingletonSynchronize {
    private static LazySingletonSynchronize instance = null;

    private LazySingletonSynchronize() {
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    //双重检测锁模式的 懒汉式单例 DCL(Double-Checked Locking)懒汉式
    public static LazySingletonSynchronize getInstance() {
        if (instance == null) { //第一次
            synchronized (LazySingletonSynchronize.class) {
                if (instance == null) { //第二次
                    instance = new LazySingletonSynchronize();
                }
            }
        }
        return instance;
    }
}

代码中getInstance()方法有两次 if (instance == null)判断,专业术语叫做 DCL

  • synchronized之前第一次判断是当instance已经创建好单例对象后,避免使用synchronized带来额外的锁开销
  • synchronized包括的代码块中第二次判断是:当线程a, 线程b同时运行到第一次if (instance == null)时,都是 null
    • 假设线程a获取锁成功,并创建实例化对象,然后释放锁
    • 线程b然后获取锁成功,如果没有第二次if (instance == null)判断的逻辑,那么它也会创建一个新的实例化对象

这个时候代码貌似从逻辑上看起来没有问题,是不是解决了线程安全问题了呢?

我们写了一个 Java 程序,我们会默认期望这些语句的实际运行顺序和写的代码顺序一致。 但是实际上编译器、JVM 或者 CPU 都有可能出于优化等目的,对于实际指令执行的顺序进行调整,这就是 重排序

重排序

举个🌰简单了解一下

重排序前

image.png

重排序后

image.png 回到我们的这个例子中 instance = new LazySingletonSynchronize():

public class LazySingletonSynchronize {
    private static LazySingletonSynchronize instance = null;

    private LazySingletonSynchronize() {
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    //双重检测锁模式的 懒汉式单例 DCL(Double-Checked Locking)懒汉式
    public static LazySingletonSynchronize getInstance() {
        if (instance == null) {  //①
            synchronized (LazySingletonSynchronize.class) {
                if (instance == null) {
                    instance = new LazySingletonSynchronize(); //②
                }
            }
        }
        return instance;
    }
}

我们期望执行的顺序是:

  1. 分配一块内存 M;
  2. 在内存 M 上初始化 LazySingletonSynchronize 对象;
  3. 指令3:然后将 M 的地址赋值给 instance 变量(此时instance也就不等于null了)

但由于重排序可能实际执行的顺序是:

  1. 分配一块内存 M;
  2. 将 M 的地址赋值给instance 变量;
  3. 在内存 M 上初始化 LazySingletonSynchronize 对象。

当线程a执行完第2步时,instance不等于 null了,这时候线程b如果执行到①时,由于 if (instance == null)不成立,不会进入到if逻辑中,会直接返回实例化一半的instance对象,这就会导致程序后面使用instance时失败。

如何解决这个问题呢?

是时候轮到volatile闪亮登场了。 volatile指令可以禁止指令重排序,避免了重排序带来的问题。

懒汉模式:synchronized + volatile

示例代码

public class LazySingletonVolatile {
    private static volatile LazySingletonVolatile instance = null;

    private LazySingletonVolatile() {
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    //双重检测锁模式的 懒汉式单例 DCL(Double-Checked Locking)懒汉式
    public static LazySingletonVolatile getInstance() {
        //第一次判断为了避免非必要加锁
        if (instance == null) {
            synchronized (LazySingletonVolatile.class) {
                if (instance == null) {
                    instance = new LazySingletonVolatile();
                }
            }
        }
        return instance;
    }
}

修改部分,使用volatile修饰变量instance private static volatile LazySingletonVolatile instance = null;

懒汉模式:静态内部类/登记式

public class SingletonInnerClass {
    private static class Holder {
        //类初始化时才执行(类只会初始化一次,实例化多次):静态初始化器,由JVM来保证线程安全
        private static final SingletonInnerClass instance = new SingletonInnerClass();
    }

    /**
     * 私有构造方法
     */
    private SingletonInnerClass() {
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    public static SingletonInnerClass getInstance() {
        return Holder.instance;
    }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。

  • SingletonInnerClass类第一次被加载时,并不需要去加载Holder类,只有当getInstance()方法第一次被调用时,才会去初始化instance;
  • 第一次调用getInstance()方法会导致虚拟机加载Holder类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

懒汉模式:枚举(推荐方式)

这种方式是 《Effective Java》这本书的作者Josh Bloch提倡的方式

public enum SingletonEnum {
    INSTANCE;
}

下面我们将下为什么推荐枚举类这种方式创建单例 除了枚举类方式以外,其他方式很容易被反射和序列化破坏单例

反射破坏单例

反射破坏: synchronized + volatile

LazySingletonVolatile类对应的代码在上面

测试代码

class SingletonTest {
    @Test
    public void test() {
        LazySingletonVolatile singleton01 = LazySingletonVolatile.getInstance();
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        //获取所有的构造方法,包括public,private
        Constructor<LazySingletonVolatile> constructor = LazySingletonVolatile.class.getDeclaredConstructor();
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);
        //使用空构造函数new一个实例。即使它是private的~~
        LazySingletonVolatile singleton02 = constructor.newInstance();
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【LazySingletonVolatile】构造函数方法
com.xxx.LazySingletonVolatile@604f376a
--------------------------------------------
>>> 进入【LazySingletonVolatile】构造函数方法
com.xxx.LazySingletonVolatile@5432dca2

可以看出两次获取的对象不相同了。

反射破坏:静态内部类

仅仅将LazySingletonVolatile类换成SingletonInnerClass

测试代码

class SingletonTest {
    @Test
    public void test() {
        SingletonInnerClass singleton01 = SingletonInnerClass.getInstance();
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        //获取所有的构造方法,包括public,private
        Constructor<SingletonInnerClass> constructor = SingletonInnerClass.class.getDeclaredConstructor();
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);
        //使用空构造函数new一个实例。即使它是private的~~
        SingletonInnerClass singleton02 = constructor.newInstance();
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【SingletonInnerClass】构造函数方法
com.xxx.SingletonInnerClass@7d90644f
--------------------------------------------
>>> 进入【SingletonInnerClass】构造函数方法
com.xxx.SingletonInnerClass@f810c18

可以看出两次获取的对象也是不相同的。

反射破坏: 饿汉模式

仅仅将LazySingletonVolatile类换成HungryManSingleton

测试代码

class SingletonTest {
    @Test
    public void test() {
        HungryManSingleton singleton01 = HungryManSingleton.getInstance();
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        //获取所有的构造方法,包括public,private
        Constructor<HungryManSingleton> constructor = HungryManSingleton.class.getDeclaredConstructor();
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);
        //使用空构造函数new一个实例。即使它是private的~~
        HungryManSingleton singleton02 = constructor.newInstance();
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【HungryManSingleton】构造函数方法
com.xxx.HungryManSingleton@75cacb3e
--------------------------------------------
>>> 进入【HungryManSingleton】构造函数方法
com.xxx.HungryManSingleton@61957d9c

可以看出两次获取的对象也是不相同的。

序列化破坏单例

LazySingletonVolatile类实现 Serializable接口,其他部分代码不变

public class LazySingletonVolatile implements Serializable {
    private static volatile LazySingletonVolatile instance = null;

    private LazySingletonVolatile() {
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    //双重检测锁模式的 懒汉式单例 DCL(Double-Checked Locking)懒汉式
    public static LazySingletonVolatile getInstance() {
        //第一次判断为了避免非必要加锁
        if (instance == null) {
            synchronized (LazySingletonVolatile.class) {
                if (instance == null) {
                    instance = new LazySingletonVolatile();
                }
            }
        }
        return instance;
    }
}

测试代码

class SingletonTest {
    @Test
    public void test() {
        LazySingletonVolatile singleton01 = LazySingletonVolatile.getInstance();
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        byte[] serialize = SerializationUtils.serialize(singleton01);
        Object singleton02 = SerializationUtils.deserialize(serialize);
        System.out.println(singleton02);
    }
}

输出结果

>>> 进入【LazySingletonVolatile】构造函数方法
com.xxx.LazySingletonVolatile@7fe87c0e
--------------------------------------------
com.xxx.LazySingletonVolatile@710afd47

可以看出序列化后的对象跟序列化前的是不一样的了。 其他几种单例模式可以自己测试一下,也会有序列化破坏单例的问题。。

枚举类实现的单例不会被反射和序列化破坏

避免序列化破坏

//示例代码
public enum SingletonEnum {
    INSTANCE
}

//测试代码
class SingletonTest {
    @Test
    public void test() {
        SingletonEnum singleton01 = SingletonEnum.INSTANCE;
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        byte[] serialize = SerializationUtils.serialize(singleton01);
        Object singleton02 = SerializationUtils.deserialize(serialize);
        System.out.println(singleton02);
        System.out.println(singleton01 == singleton02);
    }
}

输出结果

INSTANCE
--------------------------------------------
INSTANCE
true

避免反射破坏

//示例代码
public enum SingletonEnum {
    INSTANCE
}

//测试代码
class SingletonTest {
    @Test
    public void test() {
        SingletonEnum singleton01 = SingletonEnum.INSTANCE;
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        //获取所有的构造方法,包括public,private
        Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor();
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);
        //使用空构造函数new一个实例。即使它是private的~~
        SingletonEnum singleton02 = constructor.newInstance();
        System.out.println(singleton02);
        System.out.println(singleton01 == singleton02);
    }
}

输出结果: image.png 这个看起来是因为没有空的构造函数导致的,还并不能下定义说防御了反射攻击。那它有什么构造函数呢,可以看它的父类Enum类:

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
	// 这是它的唯一构造函数,接收两个参数(若没有自己额外指定构造函数的话~)
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    ...
}

修改后的代码,主要是以下两部分改动:

  • SingletonEnum.class.getDeclaredConstructor(String.class, int.class)
  • constructor.newInstance("test", 10)
//测试代码
class SingletonTest {
    @Test
    public void test() {
        SingletonEnum singleton01 = SingletonEnum.INSTANCE;
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        //获取所有的构造方法,包括public,private
        Constructor<SingletonEnum> constructor = SingletonEnum.class.getDeclaredConstructor(String.class, int.class);
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);
        //使用空构造函数new一个实例。即使它是private的~~
        SingletonEnum singleton02 = constructor.newInstance("test", 10);
        System.out.println(singleton02);
        System.out.println(singleton01 == singleton02);
    }
}

输出结果: Cannot reflectively create enum objects image.png 查看constructor.newInstance() 方法内部逻辑,如果有枚举类直接会拦截 image.png 总结一下枚举类实现的实例化代码优点:

  1. 反射安全
  2. 序列化/反序列化安全

如果我就是不用枚举类方式,其他方式还有机会弥补吗?

不好意思,有时候真的是绝人之路 网上有一种说法:
public class SingletonVolatileOptimization {
    private static volatile SingletonVolatileOptimization instance = null;
    private static boolean isFirst = true;

    private SingletonVolatileOptimization() {
        //防止反射
        synchronized (SingletonVolatileOptimization.class) {
            if (isFirst) {
                isFirst = false;
            } else {
                throw new RuntimeException("正在尝试通过反射破坏单例模式");
            }
        }
        System.out.printf(">>> 进入【%s】构造函数方法%n", getClass().getSimpleName());
    }

    //双重检测锁模式的 懒汉式单例 DCL(Double-Checked Locking)懒汉式
    public static SingletonVolatileOptimization getInstance() {
        //第一次判断为了避免非必要加锁
        if (instance == null) {
            synchronized (SingletonVolatileOptimization.class) {
                if (instance == null) {
                    instance = new SingletonVolatileOptimization();
                }
            }
        }
        return instance;
    }
}

构造函数里面新增共享资源 isFirst,无论是SingletonVolatileOptimization.getInstance()还是 constructor.newInstance()都会执行到构造函数,理论上我构造函数设置一个共享资源是不是可以防止反射破坏单例呢? 不好意思,有时候可能“魔高一尺,道高一丈”并不成立

破坏代码

通过改变isFirst的值绕过拦截:

  • Field field = SingletonVolatileOptimization.class.getDeclaredField("isFirst");
  • field.set("isFirst", true);
//测试代码
class SingletonTest {
    @Test
    public void test() {
        SingletonVolatileOptimization singleton01 = SingletonVolatileOptimization.getInstance();
        System.out.println(singleton01);
        System.out.println("--------------------------------------------");

        Field field = SingletonVolatileOptimization.class.getDeclaredField("isFirst");
        field.setAccessible(true);

        //获取所有的构造方法,包括public,private
        Constructor<SingletonVolatileOptimization> constructor = SingletonVolatileOptimization.class.getDeclaredConstructor();
        //默认为false: 设置为true时可以跳过权限检查,这就可以访问一些之前没有权限的类的方法或者类的属性
        constructor.setAccessible(true);

        field.set("isFirst", true);
        SingletonVolatileOptimization singleton02 = constructor.newInstance();

        System.out.println(singleton02);
        System.out.println(singleton01 == singleton02);
    }
}

输出结果:

>>> 进入【SingletonVolatileOptimization】构造函数方法
com.example.springboottool.service.singleton.SingletonVolatileOptimization@7d90644f
--------------------------------------------
>>> 进入【SingletonVolatileOptimization】构造函数方法
com.example.springboottool.service.singleton.SingletonVolatileOptimization@59f3426f
false

两次获取到实例化不一样了,证明还是没有避免反射的破坏。