欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单例模式五种写法与线程安全

程序员文章站 2022-06-03 18:18:27
...

单例模式五种写法与线程安全

懒汉式

是否 Lazy 初始化:是

是否多线程安全:可以实现

实现难度:易

优点:第一次调用才初始化,避免内存浪费。

缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。

单线程

public class LazyMan {
    //线程不安全
    private LazyMan(){
    }
    
    private static LazyMan instance;
    
    public static LazyMan getInstance(){
        if (instance==null){
            instance = new LazyMan();
        }
        return instance;
    }
}

多线程

public class LazyManSafe {
    //线程安全,但是锁占用资源
    private LazyManSafe(){
    }

    private static LazyManSafe instance;

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

饿汉式

是否 Lazy 初始化:否

是否多线程安全:是

实现难度:易

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

public class HungryMan {
    //线程安全,但是浪费资源空间
    private HungryMan(){
    }

    //注意加上final
    private static final HungryMan instance = new HungryMan();

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

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

双重锁/双重校验锁

JDK 版本:JDK1.5 起

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:较复杂

public class DoubleCheck {

    private DoubleCheck(){
    }

    //注意加上volatile
    private volatile static DoubleCheck instance;

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

        return instance;
    }
}

1.双重判断的原因:

第一次:避免不必要的线程等待,相当于对多线程懒汉式的优化,提高效率

第二次:避免多次实例化,单例模式的基本检查

2.使用volatile的原因:

主要是禁止重排序,初始化一个实例(SomeType st = new SomeType())在java字节码中会有4个步骤:

1、申请内存空间
2、初始化默认值(区别于构造器方法的初始化)
3、执行构造器方法
4、连接引用和实例

这4个步骤后两个有可能会重排序,1234或1243都有可能,造成未初始化完全的对象发布。volatile可以禁止指令重排序,从而避免这个问题。

3.为什么是JDK1.5起?

(待补充,大致是因为JVM虚拟机、volatile在jdk1.5前后有改变)

4.为什么synchronized保证有序性,却还要用volatile?

关于这个问题,可以参考知乎Java synchronized 能防止指令重排序吗?

静态内部类

是否 Lazy 初始化:是

是否多线程安全:是

实现难度:一般

//静态内部类
public class Holder {
    private Holder(){
    }

    static class InnerHolder{
        private static final Holder instance = new Holder();
    }

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

只有第一次调用getInstance方法时,虚拟机才加载 内部类 并初始化 instance ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。目前此方式是所有单例模式中最推荐的模式,但具体还是根据项目选择。

如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

枚举

JDK 版本:JDK1.5 起

是否 Lazy 初始化:否

是否多线程安全:是

实现难度:易

public enum Singleton {
    INSTANCE;
}

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。实际上

  • 枚举类隐藏了私有的构造器。
  • 枚举类的域 是相应类型的一个实例对象
    那么枚举类型日常用例是这样子的:
public enum Singleton  {
    INSTANCE 
 
    //doSomething 该实例支持的行为
      
    //可以省略此方法,通过Singleton.INSTANCE进行操作
    public static Singleton get Instance() {
        return Singleton.INSTANCE;
    }
}

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。

结论:

一般情况下,不建议使用第 1 种懒汉方式,建议使用第 2 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 4 种静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用第 5 种枚举方式。

对于第3种双重锁单例模式,本人认为加了volatile后应该解决了问题,可以算的上是一个线程安全、高效、懒加载的单例模式,但是搜索网上评论后大多都没有推荐这种方式,如有读者知道原因,请在评论区指出。