Java23种设计模式-创建型模式之单例模式

发布于:2024-04-26 ⋅ 阅读:(18) ⋅ 点赞:(0)

单例模式(Singleton Pattern):通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例),该类负责创建自己的对象,同时确保只有单个对象被创建。

1、单例类只能一个实例
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

Java中两种构建方式:
懒汉式线程不安全:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作
懒汉式线程安全加上synchronize之类保证线程安全的基础上的懒汉模式,相对性能很低,大部分时间并不需要同步饿汉方式。指全局的单例实例在类装载时构建。
饿汉式:这种方式比较常用,但容易产生垃圾对象。优点:没有加锁,执行效率会提高。缺点:类加载时就初始化,浪费内存。基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果
双检锁/双重校验锁(DCL,即 double-checked locking):在懒汉式基础上利用synchronize关键字和volatile关键字确保第一次创建时没有线程间竞争而产生多个实例,仅第一次创建时同步,性能相对较高。
登记式/静态内部类:作为创建类的全局属性存在,创建类被装载时创建。
枚举:java中枚举类本身也是一种单例模式

应用场景包括需要控制实例数量的类,如数据库连接池配置管理日志记录等。单例模式的主要优点包括防止多次实例化节省资源提供全局访问点以及减少全局状态的复杂性。然而,它也有一些缺点,比如可能导致代码难以测试和维护,以及在多线程环境中可能存在线程安全问题

单例模式的实现通常包括将构造方法设置为私有,以防止外部代码创建实例。它还可能包括一个私有的静态实例成员以及一个公共的静态方法用于获取这个实例。此外,双重检查锁定(double-checked locking)是一种常用的实现线程安全单例的方法。

示例

//懒汉式,线程不安全
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if (instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}
//懒汉式 线程安全
public class Singleton {
    private static Singleton instance;
    public Singleton() {
    }
    public static synchronized Singleton getInstance(){
        if (instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}
//饿汉式 线程安全
public class Singleton {
    private static Singleton instance=new Singleton();
    public Singleton() {
    }
    public static Singleton getInstance(){
        return instance;
    }
}
//双检锁/双重校验锁(DCL,即 double-checked locking)
public class Singleton {
    private volatile static Singleton singleton;
    public Singleton() {
    }
    public static Singleton getSingleton(){
        if (singleton==null){
            synchronized (Singleton.class){
                if (singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

//登记式/静态内部类
public class Singleton {
    private static class SingletonHolder{
        private static final Singleton INSTANCE=new Singleton();
    }
    public Singleton() {
    }
    public static final Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}
//枚举
public enum Singleton {
    INSTANCE;
    public void test(){
        System.out.println("hi~");
    }
}
//CAS「AtomicReference」(线程安全)
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();

    private Singleton() {
    }
    public static final Singleton getInstance() {
        for (; ; ) {
            Singleton instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null, new Singleton());
            return INSTANCE.get();
        }
    }
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance()); 
        System.out.println(Singleton.getInstance()); 
    }
}

一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式

增加:CAS「AtomicReference」(线程安全)
java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference。
AtomicReference 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。