单例模式8种写法

发布时间 2023-06-03 11:53:57作者: Java知者

0. 为什么需要单例模式?

  • 节省内存和计算
  • 保证结果正确
  • 方便管理

使用场景:

1. 饿汉式(静态常量)—推荐指数:★★☆☆☆

优点:不会有线程安全问题。
缺点:在类加载的时候就创建对象,如果一直没使用到该对象的话,就造成了内存浪费,如果对象初始化的工作有很多,也会影响到性能。

代码展示:

//饿汉式(静态常量) ---可用
public class Singleton1 {
    // 类加载时就创建该实例
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1(){}

    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

2. 饿汉式(静态代码块) —推荐指数:★★☆☆☆

优缺点和第一种方式基本一致。

//饿汉式(静态代码块) ---可用
public class Singleton2 {
    private final static Singleton2 INSTANCE;
    // 以静态代码快的形式创建实例
    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2(){}

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

3. 懒汉式(线程不安全)—推荐指数:☆☆☆☆☆

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:有线程安全问题。

//懒汉试(线程不安全)---不可用
public class Singleton3 {

    private static Singleton3 INSTANCE;

    private Singleton3(){}

    public static Singleton3 getInstance(){
        if (INSTANCE == null){
            // 当多个线程同时执行到这里,就会创建多个实例,那各自线程的实例就不是同一个了
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

4. 懒汉式(线程安全,同步方法)—推荐指数:★☆☆☆☆

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:没有线程安全问题,但是有很大的性能问题,当多个线程同时到达getInstance()方法时,需要排队进入。

这个是在第 3 步的基础上实现的,使用 synchronized 修饰静态方法,由于加上了同步工具类,同一时间只能有一个线程操作,也使得性能下降,所以也不推荐使用:

//懒汉试(线程安全)---不推荐
public class Singleton4 {

    private static Singleton4 INSTANCE;

    private Singleton4(){}

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

5. 懒汉式(线程不安全,同步代码块)—推荐指数:☆☆☆☆☆

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:线程不安全,还有性能问题!多个线程在synchronized那一行排队,进入代码块后一样会创建多个对象。

这个是在第 4 步的基础上作进一步尝试,虽然性能上的问题解决了,但是又出现了线程不安全的问题,如下:

//懒汉试(线程不安全,同步代码块)---不可用
public class Singleton5 {

    private static Singleton5 INSTANCE;

    private Singleton5(){}

    public static Singleton5 getInstance(){
        if (INSTANCE == null){
            // 如果多个线程同时执行到这里,依然会创建多个实例
            synchronized (Singleton5.class){
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

6. 懒汉式(双重检查)—推荐指数:★★★★☆

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题。
缺点:复杂。

//双重检查---推荐使用
public class Singleton6 {

    private static Singleton6 INSTANCE;

    private Singleton6(){}

    public static Singleton6 getInstance(){
        if (INSTANCE == null){
            synchronized (Singleton6.class){
                //再做一次检查:由于同时只能有一个线程进入到这里,所以此时的 INSTANCE 如果还是 null,那么就再创建,创建之后有且仅有这一个实例
                if (INSTANCE == null){
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

这个优化我们利用了双重检测机制和同步锁,这种方式也称为双重同步锁单例模式,但是这个案例还是线程不安全的,大家通过代码层面的分析后,发现确实不会有线程安全问题,那问题出现在哪呢?这个其实要和对象创建步骤和JVM 指令重排挂钩,我们正常创建对象的指令步骤是这样的:

  • memory = allocate() 分配对象的内存空间
  • ctorInstance() 初始化对象,执行对应的构造方法
  • instance = memory 设置instance指向刚分配的内存

但是因为JVM和cpu优化,发生了指令重排,执行顺序如下:

  • memory = allocate() 分配对象的内存空间
  • instance = memory 设置instance指向刚分配的内存
  • ctorInstance() 初始化对象

我们可以结合代码,假如A线程进入同步代码块执行 instance = new Singleton6(),执行到“instance = memory 设置instance指向刚分配的内存”,这个时候B线程在第一次执行“if (instance == null)”,发现instance不为空,直接返回instance实例,其实线程B得到的这个实例并没有完全初始化(A还没有执行完对象的初始化步骤)就已经使用了。

那如何禁止指令重排呢,很简单,用我们前面文章提到的volatile关键字就可以了

在 INSTANCE 前加上 volatile 关键字来修饰,代码如下:

//双重检查---推荐使用
public class Singleton6 {

    private volatile static Singleton6 INSTANCE;

    private Singleton6(){}

    public static Singleton6 getInstance(){
        if (INSTANCE == null){
            synchronized (Singleton6.class){
                //再做一次检查:由于同时只能有一个线程进入到这里,所以此时的 INSTANCE 如果还是 null,那么就再创建,创建之后有且仅有这一个实例
                if (INSTANCE == null){
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

7. 静态内部类 — 推荐指数:★★★☆☆

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题,线程安全。
缺点:没有太大缺点。

由于类在初始化时,并不会初始化静态内部类中的实例,所以这属于饿汉式单例:

//静态内部类 --- 推荐使用
public class Singleton7 {

    private Singleton7(){}

    private static class SingletonInstance{
        //JVM会保证构造方法的线程安全问题,即使多个线程同时访问 getInstance() 方法,也只会创建一个实例,这是 JVM 保证的 
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

8. 枚举 — 推荐指数:★★★★★

优点:使用到的时候才会创建对象,不会造成各种资源浪费问题,线程安全。
缺点:最优方案。

//枚举 --- 生产实践推荐使用
public enum Singleton8 {
    INSTANCE;

    //方法
    public void whatever(){ }
}

使用的时候只需要调用Singleton8.INSTANCE ,比如这里想调用该类中的 whatever()方法,只需要执行 Singleton8.INSTANCE.whatever()

文章来源:单例模式8种写法

个人微信:CaiBaoDeCai

微信公众号名称:Java知者

微信公众号 ID: JavaZhiZhe

谢谢关注!