设计模式 ---- 单例模式 (Singleton Pattern)

介绍

单例模式(Singleton Pattern)是java 中最简单的设计模式之一。这类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象

单例的要求

  • 构造方法必须私有化(确保只有自己能创建)
  • 以静态方法返回实例(外界不能通过new来获取对象)
  • 确保对象实例只有一个(只对类进行一次实例化,以后都直接获取第一次实例化的对象)

单例实现

饿汉式(线程安全)

使用类的时候已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说),常见的实现办法就是直接new实例化

public class Singleton {

    /**
     * 将自身实例化对象设置为一个属性,并用static、final修饰
     */
    private static final Singleton instance = new Singleton();

    /**
     * 构造方法私有化
     */
    private Singleton(){}

    /**
     * 静态方法返回该实例
     * @return
     */
    public static Singleton getInstance(){
        return instance;
    }

}
饿汉模式的优点 & 缺点

优点:实现起来简单,没有多线程同步问题

缺点:当类Singleton被加载的时候,会初始化static的instance,静态变量被创建并分配内存空间,从这以后,这个static的instance对象便一直占着这段内存(即便你还没有用到这个实例),当类被卸载时,静态变量被摧毁,并释放所占有的内存,因此在某些特定条件下会耗费内存

懒汉模式(线程不安全)

懒汉模式调用get()方法时才创建(先不着急实例化出对象,等到要用的时候才给你创建。不着急,故称为“懒汉式”),常见的实现方法就是在get方法中进行new实例

public class Singleton {

    /**
     * 将自身实例化对象设置为一个属性,并用static修饰
     */
    private static Singleton instance;

    /**
     * 构造方法私有化
     */
    private Singleton(){}

    /**
     * 静态方法返回该实例
     * @return
     */
    public static Singleton getInstance() {

        if(instance == null){

            // 模拟出现初始化耗时操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            instance = new Singleton();
        }
        return instance;
    }

}
public class Demo {

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> System.out.println(Singleton.getInstance().hashCode()));
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
        }


      }
}
懒汉模式存在的问题

线程安全问题:

图片也很清楚,多线程的情况下,可能存在这样的问题:一个线程判断instance == null,开始初始化对象;还没来得及初始化对象时候,另一个线程访问,判断instance ==null,也创建对象。最后的结果,就是实例化了两个Singleton对象。

懒汉模式的优点 & 缺点

优点:实现起来比较简单,当类Singleton被加载的时候,静态变量static的instance未被创建并分配内存空间,当getInstance方法第一次被调用时,初始化instance变量,并分配内存,因此在某些特定条件下会节约了内存

缺点:在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态

懒汉模式(加锁)

public class Singleton {

    // 将自身实例化对象设置为一个属性,并用static修饰
    private static Singleton instance;
    
    // 构造方法私有化
    private Singleton() {}
    
    // 静态方法返回该实例,加synchronized关键字实现同步
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
懒汉模式(加锁)存在的问题

这种把锁直接方法上的办法,所有的访问都需要获取锁,会导致了资源的浪费

懒汉模式(加锁)的优点 & 缺点

优点:在多线程情形下,保证了“懒汉模式”的线程安全

缺点:众所周知在多线程情形下,synchronized方法通常效率低,显然这不是最佳的实现方案

懒汉模式(双重校验锁)

public class Singleton {

    /**
     * 将自身实例化对象设置为一个属性,并用static修饰
     * volatile修饰,防止指令重排
     *
     */
    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 修饰 instance?

为了防止指令重排,指令重排指的是Instance = new Singleton(),我们感觉是一步操作的实例化对象,实际上对于JVM指令,是分为三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

有些编译器为了性能优化,可能会把第三步和第二步进行重排序,顺序成了:

  1. 分配内存空间
  2. 将对象指向刚分配好的内存空间
  3. 初始化对象

在这里插入图片描述
所以不使用Volatile防止指令重排可能会发生什么情况呢?
在这里插入图片描述
这种情况下,T7 时该线程B对Instance 的访问,访问的是一个未初始化未完成的对象

所以需要在instance前面加入关键字 volatile

  • 使用了volatile关键字后,可以保证有序性,指令重排被禁止
  • volatile还可以保证可见性,java内存模型会确保所以线程看到的变量值是一致的

单例模式(静态内部类)

public class Singleton {

    private Singleton() {
    }

    private static class InnerSingleton {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InnerSingleton.instance;
    }
}

内部静态类是更进一步的写法,不仅能实现懒加载,线程安全,而且JVM还保持了指令优化的能力

Singleton类被装载时并不会实例化,而是在需要实例化时,调用getInstance()方法,才会加载静态内部类InnerSingleton类,从而完成Singleton的实例化

类的静态属性会在第一次加载类的时候初始化,同时类加载的过程又是线程互斥的,JVM帮助我们保证了线程安全

单例模式(CAS)

public class Singleton {

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

    private Singleton() {
    }

    public static final Singleton getInstance() {
        //等待
        while (true) {
            Singleton instance = INSTANCE.get();
            if (null == instance) {
                INSTANCE.compareAndSet(null, new Singleton());
            }
            return INSTANCE.get();
        }
    }
}

这种CAS式的单例模式算是懒汉式直接加锁的一个变种,sychronized是一种悲观锁,而CAS是乐观锁,相对比较轻级

当然,这种写方法也比较罕见,CAS存在忙等的问题,可能会造成CPU的浪费

单例模式(枚举)

public enum Singleton {

    //定义一个枚举,代表了Singleton的一个实例
    INSTANCE;
    public void anyMethod(){
        System.out.println("do any thing");
    }
}

这种写法解决了最主要的问题:线程安全、⾃由串⾏化、单⼀实例

总结

从使用的角度来讲,如果不需要懒加载的话,直接用饿汉式就行了;如果需要懒加载,可以考虑内部类,或者尝试一下枚举的方式

从面试的角度,饿汉式,懒汉式,双重校验锁饿汉式,这三个是重点。双重校验锁一定要知道指令重排是在哪,会导致什么问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值