单例模式

核心概念

单例模式包装一个类只有一个实例,并且提供一个全局访问点来访问该实例

要求优点
确保一个类只有一个实例避免重复创建对象,避免资源浪费和冲突
提供了一个全局访问点来访问该实例。只有一个全局访问点,控制了访问权限

饿汉式

  1. 确保只有一个实例
    1. 在加载类的时候就创建一个静态实例(与懒汉式的不同)
    2. 避免创建实例
  2. 提供了全局唯一访问点来获取对象
public class Hungry {
    //1. 用private修饰,确保只能通过public方法来访问
    //2. 用static修饰,HUNGRY属于这个类,保证全局只有一个实例
    //3. 用final修饰,保证HUNGRY初始化后无法修改
    private static Hungry HUNGRY = new Hungry();

    //因为默认的构造方法为共有的
    //为了避免重复创建对象,这里的构造方法必须显式的定义为private
    private Hungry(){}

    //这里的static是提供全局唯一访问点
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

懒汉式(内部类)

利用JVM在加载类时并不会加载内部类,但调用时,则开始加载内部类来创建实例

public class Lazy1 {
    //内部静态类
    private static class InnerClass{
        private static Lazy1 instance = new Lazy1();
    }    
    
    //私有防止创建对象
    private Lazy1(){}

    //public和static是为了让外部调用
    public static Lazy1 getInstance(){
        return InnerClass.instance;
    }
}

懒汉式(双重检查锁)

版本一

public class Lazy {
    private static Lazy lazy;
    private Lazy(){}
 
    public static Lazy getInstance(){
        //对lazy进行判断,如果为空就创建,不为空就直接返回
        if(lazy == null){
            lazy = new Lazy();
        }
        return lazy;
    }
}

缺点:如果多个并发线程同时进入getInstance() ,此时多个线程同时判断lazy==null ,还是会创建多个对象

版本二

public class Lazy {
    private static Lazy lazy;
    private Lazy(){}

    //加了个synchronized修饰,保证每次只有一个线程才可以进入该方法
    public static synchronized Lazy getInstance(){
        //对lazy进行判断,如果为空就创建,不为空就直接返回
        if(lazy == null){
            lazy = new Lazy();
        }
        return lazy;
    }
}

每次只有一个线程进入getInstance() ,保证不会创建多个对象

缺点:锁的粒度太大了,每次获得lazy都要进行加锁,效率太低

版本三

public class Lazy {
    //volatile是保证lazy的可见性
    private volatile static Lazy lazy;
    private Lazy(){}
    //这里避免了每一次加锁,如果有实例,那么只需要进行一次判断即可,不用加锁
    public static Lazy getInstance(){
        if(lazy == null){
            //如果lazy为null,就锁住Lazy这个类
            synchronized (Lazy.class){
                //为什么还要判断一次呢,因为可能在synchronized加锁的过程中,其他线程创建了lazy实例
                //这里再判断一次,防止重复创建
                if(lazy == null){
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }
}

这样即不会重复创建对象,又不会使锁的粒度过大

双重检查锁

双重检查锁是一种优化多线程实例化单例模式的技巧。它分为两次判断 instance == null,这样做的目的是为了避免每次获取实例时都进行同步,从而提高性能。具体的步骤如下:

  • 第一次判断:检查 lazy 是否为 null,如果不为 null,说明实例已经创建,直接返回已有实例,不需要进入同步代码块,避免了同步的性能开销。
  • 第二次判断:如果进入了同步代码块,其他线程已经可能在等待或者进入该同步块执行实例化的操作。为了确保在高并发情况下只会有一个线程实例化 Lzay,需要再次判断 lazy == null,避免多个线程同时创建实例。

为什么要加锁?

在判断lazy==null 后进行加锁,保证只有一个线程可以进入代码块,其他线程会被阻塞,直到实例化完成。

为什么第一次判断不加锁,第二次判断加锁?

  • 第一次判断不加锁:如果实例已经创建,直接返回实例,这样可以避免每次访问 getInstance() 都需要进行加锁,提高性能。
  • 第二次判断加锁:只有在第一次判断为 null 时,才会进入加锁区域。在加锁区域里,再次判断 instance == null 是为了确保只有一个线程创建实例。即使多个线程进入同步块,也会通过第二次判断避免重复创建实例。

枚举(天然的单例模式)

public enum Singleton {
    
    INSTANCE;
    
}

比较是偷走幸福的小偷