设计模式——单例模式

  • 作用:单例模式主要解决的是,防止一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
  • 特点:单例模式有一个特点就是不允许外部直接创建,因此在默认的构造函数上添加了私有属性 private

懒汉式(线程不安全)

/**
 * 懒汉式
 */
public class Lazy {

    private static Lazy instance;

    /**
     * 私有构造方法,防止被实例化
     */
    private Lazy(){
    }

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

优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率

缺点:存在并发访问的问题(可能进行了多次new操作)


懒汉式(线程安全)

/**
 * 懒汉式
 */
public class Lazy {

    private static Lazy instance;

    private Lazy(){
    }

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

拥有上面懒汉式的优点,同时也克服了其缺点,使用synchronized关键字同步加锁,保证了线程安全,但所有的访问都需要加锁,造成了资源的浪费。


饿汉式(线程安全)

/**
 * 饿汉式
 */
public class Hungry {

    private static Hungry hungry = new Hungry();

    /**
     * 私有构造方法,防止被实例化
     */
    private Hungry(){
    }

    public static Hungry getInstance(){
        return hungry;
    }
}

优点:static变量会在类装载时初始化,不存在并发访问问题,可以省略synchronized关键字

缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费


双重校验锁(线程安全)

/**
 * 双重校验锁
 */
public class DoubleLock {

    private static volatile DoubleLock instance;

    private DoubleLock(){
    }

    public static DoubleLock getInstance(){
        if(instance != null)
            return instance;
        synchronized (DoubleLock.class){
            if(instance == null)
                instance = new DoubleLock();
        }
        return instance;
    }
}
  • 双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
  • 同时这种方式也满足了懒加载。

为什么使用volatile

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

  1. 为 singleton 分配内存空间

  2. 初始化 singleton

  3. 将 singleton 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1-3-2。指令重排在单线程环境下不会出 现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 singleton 不为空,因此返回 singleton ,但此时 singleton 还未被初始化。 使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。


静态内部类(线程安全)

/**
 * 静态内部类
 */
public class InnerClass {

    private static class inner{
        private static InnerClass instance = new InnerClass();
    }

    private InnerClass(){
    }

    public static InnerClass getInstance(){
        return inner.instance;
    }
}
  • 既保证了线程安全又保证了懒加载,同时不会因为加锁的方式耗费性能。
  • 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。

CAS(线程安全)

/**
 * CAS
 */
public class CAS {

    private static final AtomicReference<CAS> INSTANCE = new AtomicReference<>();
    
    private CAS(){
    }

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

枚举单例(线程安全)

/**
 * 枚举单例
 */
public enum EnumSingle {

    INSTANCE;

    public void test(){
        System.out.println("枚举单例");
    }

    public static void main(String[] args) {
        EnumSingle.INSTANCE.test();
    }
}
  • 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
  • 缺点:无延迟加载

参考资料

重学 Java 设计模式:实战单例模式「7种单例模式案例,Effective Java 作者推荐枚举单例模式」 - bugstack虫洞栈

彻底玩转单例模式

上一篇:单例模式 Java


下一篇:C#实现单例模式的几种方法总结