详谈单例、饿汉、和懒汉模式

一、基本概念

单例模式属于创建型设计模式。

确保一个类只有一个实例,并提供该实例的全局访问点。

实现: 使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现

二、结构

类图:



私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量

三、几类经典单例模式实现

1、懒汉式-线程不安全

下面的实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance == null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance

// 懒汉式: 线程不安全
// 有延迟加载: 不是在类加载的时候就创建了,而是在调用newStance()的时候才会创建
public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton(){

    }

    public static Singleton newInstance(){
        if(uniqueInstance == null)
            uniqueInstance = new Singleton();
        return uniqueInstance;
    }
}复制代码

2、懒汉式-线程安全-性能不好

为了解决上面的问题,我们可以直接在newInstance()方法上面直接加上一把synchronized同步锁。那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用

public static synchronized Singleton newInstance(){//在上面的基础上加了synchronized
    if(uniqueInstance == null)
        uniqueInstance = new Singleton();
    return uniqueInstance;
}复制代码

3、饿汉式-线程安全-无延迟加载

饿汉式就是 : 采取直接实例化 uniqueInstance 的方式,这样就不会产生线程不安全问题。

这种方式比较常用,但容易产生垃圾对象(丢失了延迟实例化(lazy loading)带来的节约资源的好处)。

它基于 classloader机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazyloading 的效果

public class Singleton {

    // 急切的创建了uniqueInstance, 所以叫饿汉式
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){
    }

    public static Singleton newInstance(){
        return uniqueInstance;
    }

    // 瞎写一个静态方法。这里想说的是,如果我们只是要调用 Singleton.getStr(...),
    // 本来是不想要生成 Singleton 实例的,不过没办法,已经生成了
    public static String getStr(String str) {return "hello" + str;}
}复制代码

4、双重校验锁-线程安全

uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当uniqueInstance 没有被实例化时,才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁

// 双重加锁
public class Singleton {

    // 和饿汉模式相比,这边不需要先实例化出来
    // 注意这里的 volatile,使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton newInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}复制代码
注意,内层的第二次if (uniqueInstance == null) {也是必须的,如果不加: 也就是只使用了一个 if 语句。在 uniqueInstance == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton();这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  • 1)、为 uniqueInstance 分配内存空间;
  • 2)、初始化 uniqueInstance
  • 3)、将 uniqueInstance 指向分配的内存地址;

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 newInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

5、静态内部类实现

Singleton 类加载时,静态内部类 Holder 没有被加载进内存。只有当调用 newInstance()方法从而触发 Holder.uniqueInstanceHolder 才会被加载,此时初始化 uniqueInstance实例,并且 JVM 能确保 uniqueInstance只被实例化一次。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

这种方式是 Singleton 类被装载了,uniqueInstance 不一定被初始化。因为 Holders 类没有被主动使用,只有通过显式调用 newInstance() 方法时,才会显式装载 Holder 类,从而实例化 uniqueInstance
public class Singleton {

    private Singleton() {
    }

    // 主要是使用了 嵌套类可以访问外部类的静态属性和静态方法 的特性
    // 很多人都会把这个嵌套类说成是静态内部类,严格地说,内部类和嵌套类是不一样的,它们能访问的外部类权限也是不一样的。
    private static class Holder {
        private static final Singleton uniqueInstance = new Singleton();
    }
    public static Singleton newInstance() {
        return Holder.uniqueInstance;
    }
}复制代码

6、枚举实现

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

该实现在多次序列化再进行反序列化之后,不会得到多个实例。而其它实现需要使用 transient修饰所有字段,并且实现序列化和反序列化的方法。

枚举实现单例 (+测试):

public class Singleton {

    private Singleton() {

    }

    public static Singleton newInstance() {
        return Sing.INSTANCE.newInstance();
    }

    private enum Sing {

        INSTANCE;

        private Singleton singleton;

        //jvm guarantee only run once
        Sing() {
            singleton = new Singleton();
        }

        public Singleton newInstance() {
            return singleton;
        }
    }

    public static int clientTotal = 1000;

    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Semaphore semaphore = new Semaphore(threadTotal);
        CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

        Set<Singleton>set = Collections.synchronizedSet(new HashSet<>());//注意set也要加锁

        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();

                    set.add(Singleton.newInstance());

                    semaphore.release();
                } catch (Exception e) {

                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();

        System.out.println(set.size());//1
    }
}复制代码

关于序列化和反序列化:

public enum Singleton {

    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}复制代码

测试:

public class Test {

    public static void main(String[] args){
        // 单例测试
        Singleton s1 = Singleton.INSTANCE;
        s1.setName("firstName");
        System.out.println("s1.getName(): " + s1.getName());

        Singleton s2 = Singleton.INSTANCE;
        s2.setName("secondName");

        //注意我这里输出s1 ,但是已经变成了 secondName
        System.out.println("s1.getName(): " + s1.getName());
        System.out.println("s2.getName(): " + s2.getName());

        System.out.println("-----------------");

        // 反射获取实例测试
        Singleton[] enumConstants = Singleton.class.getEnumConstants();
        for (Singleton enumConstant : enumConstants)
            System.out.println(enumConstant.getName());
    }
}复制代码

输出:

s1.getName(): firstName
s1.getName(): secondName
s2.getName(): secondName
-----------------
secondName复制代码
该实现可以防止反射攻击。在其它实现中,通过 setAccessible()(反射中的强制访问私有属性方法) 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。

四、总结

一般情况下,不建议使用懒汉方式,建议使用饿汉方式。

只有在要明确实现 lazy loading 效果时,才会使用静态内部类方式

如果涉及到反序列化创建对象时,可以尝试使用枚举方式。

如果有其他特殊的需求,可以考虑使用双检锁方式。



转载于:https://juejin.im/post/5d049310518825614732793a

### 饿汉模式与懒模式的优缺点对比 #### 饿汉模式 饿汉模式的核心特点是 **在类加载时即创建唯一的实**,因此具有以下特点: - **优点** - 实现简,易于理解维护[^1]。 - 天然线程安全,无需额外考虑多线程环境下的同步问题[^2]。 - 对象一旦被加载到内存中就不会释放,适合用于频繁使用的对象场景[^3]。 - **缺点** - 资源占用较高。即使该对象在整个程序运行期间从未被使用,也会在类加载时创建并占据内存空间[^1]。 - 不适用于需要延迟加载或者依赖外部配置动态初始化的情况[^4]。 ```java public class EagerInitializedSingleton { private static final EagerInitializedSingleton instance = new EagerInitializedSingleton(); private EagerInitializedSingleton() {} public static EagerInitializedSingleton getInstance() { return instance; } } ``` --- #### 懒模式 懒模式的主要特征是 **仅在第一次调用 `getInstance()` 方法时才创建实**,从而实现延迟加载的效果。以下是其具体特性分析: - **优点** - 延迟加载。只有当确实需要用到这个对象的时候才会去创建它,节了不必要的资源消耗[^1]。 - 更灵活。可以在实化之前执行一些必要的前置操作(比如读取配置文件等)。 - **缺点** - 默认情况下并非线程安全。多个线程同时调用 `getInstance()` 方法可能会导致多次实化的问题[^2]。 - 解决线程安全问题的方法(如加锁或双重校验锁定)会增加一定的复杂度性能开销[^3]。 ```java public class LazyInitializedSingleton { private static LazyInitializedSingleton instance; private LazyInitializedSingleton() {} public static synchronized LazyInitializedSingleton getInstance() { if (instance == null) { instance = new LazyInitializedSingleton(); } return instance; } } ``` 改进版:双检锁机制以减少同步带来的性能影响。 ```java public class ThreadSafeLazyInitializedSingleton { private static volatile LazyInitializedSingleton instance; private ThreadSafeLazyInitializedSingleton() {} public static LazyInitializedSingleton getInstance() { if (instance == null) { // 第一次检查 synchronized (ThreadSafeLazyInitializedSingleton.class) { if (instance == null) { // 第二次检查 instance = new LazyInitializedSingleton(); } } } return instance; } } ``` --- ### 综合比较表 | 特性 | 饿汉式 | 懒式 | |--------------------|------------------------------------------|-------------------------------------------| | 创建时机 | 类加载时立即创建 | 使用时按需创建 | | 线程安全性 | 自然线程安全 | 初始版本非线程安全;可通过同步解决 | | 性能 | 较高 | 同步可能导致一定性能损失 | | 是否支持延迟加载 | 否 | 是 | | 应用场景 | 对象始终会被使用 | 对象可能不会被使用 | --- ### 结论 选择何种模式取决于具体的业务需求。如果对象一定会被频繁使用,则可以选择更简饿汉式;而对于那些不确定是否会用到的对象,或者希望优化启动速度的情况下,应优先考虑懒式,并注意处理好线程安全问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值