掘金 后端 ( ) • 2024-04-16 13:44

大家好,我是徒手敲代码。

今天来介绍一下单例模式。

单例模式的意思,就是对于某一个类,只能创建一个实例对象。

饿汉式和懒汉式

首先根据概念,可以写出这样的单例模式代码 demo

public class Singleton {
    // 私有构造函数,严格限制入口
    private Singleton() {}  
    // 饿汉式
    private static final Singleton instance = new Singleton();  

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

因为目的是想只能创建一个对象,所以要把构造方法写成私有的,然后暴露一个 getInstance() 方法出来,让别的类调用这个方法来获取 Singleton 对象。

像这样还没有实际用到对象之前,就已经将对象创建出来的方式,称为饿汉式

如果说为了尽可能节省内存的开销,可以在实际需要这个对象的时候,才创建,这种方式也称为懒汉式,代码如下:

public class Singleton {
    // 私有构造函数,严格限制入口
    private Singleton() {}  
    // 懒汉式
    private static Singleton instance = null; 

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

上面的这段小程序,在多线程环境下,肯定是没办法做到单例的,如果多个线程同时进入 if 判断,同时判断出 instance 为空,那么自然地就可以创建出来多个 Singleton 对象了。

双重检测锁

在上面懒汉式的基础上,可以在判断的时候,加个锁,只能让一个线程进入 new 对象。比如:

public class Singleton {
    // 私有构造函数,严格限制入口
    private Singleton() {}  
    private static Singleton instance = null;  

    public static Singleton getInstance() {
        // 第一次检查
        if (instance == null) {      
            //加锁
            synchronized (Singleton.class) {
                // 第二次检查
                if (instance == null) {     
                    instance = new Singleton();  
                }
            }
        }
        return instance;
    }
}

这里synchronized锁的是这个Singleton的类,注意不能使用对象锁哈,因为我们目的是整个类只能创建一个对象。

为什么要做第二次检查?假设 A 线程刚刚完成了对象的构建,此时有个 B 线程进来,在刚即将创建完成和创建完成的,这个临界点,B是可以获取到锁,进去 new 对象的,所以说,一定要加多一次判断。

指令重排序

其实这段代码,也不是百分百的线程安全。编译器在将我们写的代码,转换成字节码的过程中,会趁我们不注意,来一个指令重排序的操作。

比如 instance = new Singleton(); 这个操作,在计算机的眼里,总共有这三个步骤:

  • a. 分配对象内存空间
  • b. 初始化对象成员变量
  • c. 将 instance 变量指向分配的内存地址

我们会一厢情愿的认为,执行顺序就是 a → b → c

但是,实际情况是,有可能操作顺序变成 a → c → b,那么就存在一个时刻,是 instance 变量不为空,但是对象还没有创建出来;在这个时刻,如何恰巧有一个线程进来,那么就直接去到 return instance;返回了一个处于无效状态的对象。天啊,辛辛苦苦搞过来的对象,居然是无效的!

我们在之前的文章讲过,要打破这种情况,就要加上一个volatile,加了之后的代码如下:

public class Singleton {
    // 私有构造函数,严格限制入口
    private Singleton() {}  
    private volatile static Singleton instance = null;  

    public static Singleton getInstance() {
        // 第一次检查
        if (instance == null) {      
            //加锁
            synchronized (Singleton.class) {
                // 第二次检查
                if (instance == null) {     
                    instance = new Singleton();  
                }
            }
        }
        return instance;
    }
}

其他方式

其实还有其他实现单例模式的常见方法,比如用静态内部类

public class Singleton {
    // 私有构造函数,严格限制入口
    private Singleton() {} 

    private static class SingletonHolder {
        // 静态内部类存放对象实例
        private static final Singleton INSTANCE = new Singleton();  
    }

    public static Singleton getInstance() {
        // 调用时才加载静态内部类
        return SingletonHolder.INSTANCE;  
    }
}

注意,这种方式,从外部是无法访问内部类的,只能调用getInstance()方法来获取实例对象。

以上说的这些实现单例模式的方法,都可以用反射来破解,大致步骤是:

  • 先获取Singleton的构造函数
  • 将访问权限设置成 true
  • 直接用 newInstance 方法来创建对象

代码:

        //获得构造器
        Constructor constructor = Singleton.class.getDeclaredConstructor();
        //开启访问权限
        constructor.setAccessible(true);
        //构造两个不同的对象
        Singleton singleton1 = (Singleton)constructor.newInstance();
        Singleton singleton2 = (Singleton)constructor.newInstance();
        //验证是否是不同对象
        System.out.println(singleton1.equals(singleton2));

可发现结果为 false

如果要防止被反射打破单例的实现,可以用枚举的方式,代码就一行:

public enum SingletonEnum {
    INSTANCE;
}

总结一下以上这几种方式的特点

实现方式 是否线程安全 是否懒加载 是否防止反射构建 双重检测锁 是 是 否 静态内部类 是 是 否 枚举 是 否 是

今天的分享到这里结束了,如果你喜欢这种分享知识的方式,可以在下方留言喔。

——————————————————

关注我(公众号“徒手敲代码”),让知识变得简单。

回复“电子书”,免费获取大佬推荐的Java书籍