Java设计模式:单例模式

1. 单例模式是什么?

一种最简单的最常用的设计模式,同时也是面试最常问的一种设计模式

单例模式:采取一定的方法保证在整个系统中,对某个类 只能存在一个对象实例 ,并且该类只提供一个取得其对象实例的静态方法。

单例模式 保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些 需要频繁创建销毁 的对象,使用单例模式可以提高系统性能。

单例模式 主要解决的痛点就是 设计一个 全局使用 的类频繁的去 创建和消费 ,从而提升代码性能,提高系统性能。

单例模式的使用场景 (频繁创建和消费的对象、重量级但常用的对象)

  • 数据库连接池
  • Spring Ioc中单例模式的Bean的生成和使用
  • 业务上需要设置全局的属性保存、需要一个全局的对象实例
  • 工具类对象、频繁访问数据库或文件的对象

2. 单例模式的实现方式

单例模式是如何使一个类只存在一个对象的呢?

下面介绍以下六种实现方式

  1. 饿汉式
  2. 懒汉式
  3. 静态内部类
  4. 双重锁检查
  5. CAS实现
  6. 枚举

2.1 饿汉式实现

静态常量

public class Singleton {
    
    // 静态常量
    private static final Singleton instance = new Singleton();

    // 私有化构造器
    private Singleton() {}

    // 获取单例的静态方法
    public static Singleton getInstance(){
        return instance;
    }
}
复制代码

静态代码块

public class Singleton {
   
    private static Singleton instance;
    
    // 静态代码块
    static {
        instance = new SingletonReal02();
    }
    
    // 私有化构造器
    private Singleton() {}

    // 获取单例的静态方法
    public static Singleton getInstance(){
        return instance;
    }
}
复制代码

总结

  • 为什么可以实现加载一次?
    • 因为 类加载在程序启动时进行加载 ,完成实例化,这样就避免了 线程同步 问题。
    • 后续想要拿到该实例使用时,只需要通过静态方法获取即可。
  • 缺点
    • 显而易见的这种方式并 不是懒加载 (用的时候才实例化),即无论程序中是否用到过都已经加载好了
    • 这样是会带来 内存浪费 的

2.2 懒汉式实现

线程不安全

优点:懒加载, 可以在 单线程 下使用

缺点:当有多个线程执行代码时,可能会创建多个实例,不再是单例 (即线程不安全

public class Singleton {

    private static Singleton instance;
    
    // 私有化构造器
    private Singleton() {}

    // 获取单例的静态方法
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
复制代码

线程安全 - 同步方法

优点:解决了线程安全问题

缺点:众所周知 synchronized 还是蛮影响效率的,所有线程访问时都因为锁导致资源浪费,不是很划算呢。

public class Singleton {

    private static Singleton instance;

    // 私有化构造器
    private Singleton() {}

    // 获取单例的静态方法
    public static synchronized Singleton getInstance(){
        if (null != instance) return instance;
        instance = new Singleton();
        return instance;
    }

}
复制代码

2.3 静态内部类

先说好处(●'◡'●) - 推荐使用方法之一

优点:避免了线程不安全,利用静态内部类可以实现 延迟加载

为什么能做到这些优点呢?

① 静态内部类并不会因为 Singleton 被装载时实例化,而是在调用 getInstance 方法才会使 SingletonInstance 装载从而才会完成 Singleton 的实例化。(即懒加载

② 类的静态属性只会在第一次加载类时初始化,这归功于JVM虚拟机可以保证多线程并发访问正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载(类初始化时,其他线程是无法进入的)。(即线程安全

public class Singleton {
    // 私有化构造器
    private Singleton {}
    
    // 静态内部类
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    // 获取单例的静态方法
    public static Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}
复制代码

2.4 双重锁检查

先说好处(●'◡'●) - 推荐使用方式之一

优点:线程安全、延迟加载(懒加载)、效率蛮高

Double-Check其实是对方法级锁的优化、减少了部分获取实例的耗时

public class Singleton {
    
    private static volatile Singleton instance;

    // 私有化构造器
    private Singleton() {}
    
    // 获取单例的静态方法
    public static Singleton getInstance() {
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
复制代码

一个困扰许久的问题 - 为什么使用 volatile ?

如果单说双重锁检查的话,确实 getInstance 里的代码足以满足两次检查

这个方法首先判断变量是否被初始化,没有被初始化,再去获取锁。获取锁之后,再次判断变量是否被初始化。第二次判断目的在于有可能其他线程获取过锁,已经初始化改变量。第二次检查还未通过,才会真正初始化变量。

看似确实已经很完美了,但其中还是有一些问题被忽略了

这个被忽略的问题就是 你真的了解 new 背后的指令吗?

字节码可以看到创建一个对象实例,可以分为三步:

  1. 分配对象内存
  2. 调用构造器方法,执行初始化
  3. 将对象引用赋值给变量

没错,new 在字节码层面分成了三步,最重要的是它不是一个原子指令 /(ㄒoㄒ)/

所以就会出现这样的情况:如果线程 1 获取到锁进入创建对象实例,而这个时候发生了指令重排序,完成了第一步的分配内存和第三步的变量赋值,但还没有完成第二步的初始化对象。刚好线程1 停滞了一会,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

知道了原因,那为什么要使用 volatile 呢?

因为 volatile 具有两个特性 -- 可见性、禁止指令重排

正因为 volatile 的禁止指令重排,从而使得其他线程不会访问到一个尚未初始化的对象,从而保证安全性。

总结就是:对象的创建可能发生指令的重排序,而volatile可以避免,保证多线程环境的安全性。


2.5 CAS实现

public class Singleton {

    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    // 私有化构造器
    public Singleton() {}

    // 获取单例的静态方法
    public static Singleton getInstance() {
        for (; ;) {
            Singleton instance = INSTANCE.get();
            if (instance != null) return instance;
            INSTANCE.compareAndSet(null, new Singleton());
            return INSTANCE.get();
        }
    }
}
复制代码

使用AtomicReference<Singleton>可以引用Singleton实例,提供了原子性,保证了并发访问的安全性

使用CAS的好处在于不需要使用传统的加锁方式来保证线程安全,依赖于CAS的忙等来保证线程安全,并且去除了加锁实现对线程切换和阻塞带来的额外开销,支持较大的并发

当然CAS也有缺陷:忙等的代价就是如果一直未获取会处于死循环,导致CPU开销过大


2.6 枚举

Effective Java 作者推荐使用枚举的方式 -- 枚举

好处:线程安全、自由串行化(防止反序列造成重新创建对象)

public enum Singleton {
    INSTANCE;
}
复制代码

3. 总结

  1. 看了这么多的单例模式实现方式,应该也注意到代码中注释的内容中一直在强调的
    • 第一步:私有化构造器
    • 第二步:写一个获取单例的静态方法
  2. 如果开发中并不要求懒加载,可以使用饿汉式实现单例模式
  3. 推荐使用:双重锁检查、静态内部类、枚举

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值