单例模式
Singleton
单例模式是创建型模式,通过单例模式创建的类只有一个实例存在
有时候完全只需要一个实例存在就够了,不用那么多实例存在
单例模式使用场景
- 频繁创建、销毁对象
- 创建对象耗时耗资源
- 工具类
懒汉式单例
懒汉式单例可以理解成懒加载,要使用时再加载
通常需要先私有化构造,通过方法获取时需要判断单例是否已经生成
//懒汉式 public class SingLeton02 { private SingLeton02(){ } //类加载时 不初始化 所以不加final private static SingLeton02 INSTANCE; public static SingLeton02 getInstance(){ //第一次调用这个方法时,INSTANCE为空实例化对象,之后不为空了就直接返回这个单例 if (INSTANCE == null){ /*try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }*/ INSTANCE = new SingLeton02(); } return INSTANCE; } public static void main(String[] args) { //多线程测试 发现线程不安全(测不出来可以在getInstance方法中线程睡眠) for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(SingLeton02.getInstance())).start(); } } }
线程不安全原因:当线程A进入getInstance方法判断INSTANCE为空,此时线程B也进入getInstance方法并判断INSTANCE为空,然后就造成了实例多个对象,并非单例
懒汉式加锁优化
既然懒汉式线程不安全,那就加锁
//懒汉式加锁优化 public class SingLeton03 { private SingLeton03(){ } private static SingLeton03 INSTANCE; //因为这里是同步方法,所以锁的是SingLeton03这个Class对象 public synchronized static SingLeton03 getInstance(){ if (INSTANCE == null){ INSTANCE = new SingLeton03(); } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(SingLeton03.getInstance())).start(); } } }
虽然线程安全了,但是因为加了synchronized效率变低了(每次操作都要看是否加了锁等....)
懒汉式双重检测锁DCL
多线程安全
将同步方法变为同步块再优化
//懒汉式双重锁优化 public class SingLeton04 { private SingLeton04(){ } //volatile防止指令重排序 private volatile static SingLeton04 INSTANCE; public static SingLeton04 getInstance(){ //这里的判断条件很有必要,如果没有这条判断条件,线程一进来直接抢锁降低效率 if (INSTANCE == null){ synchronized (SingLeton04.class){ if (INSTANCE == null){ INSTANCE = new SingLeton04(); } } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(SingLeton04.getInstance())).start(); } } }
在初始化对象的过程中可能存在指令重排序,如果此时对象发生逃逸可能会引起并发导致的一致性问题
初始化对象过程可分为以下三步:
1.分配空间 2.执行构造,初始化 3. 指向对象
多线程场景下,为防止JVM重排指令(逻辑上) 应该用volatile
修饰,禁止重排指令
//volatile防止重排指令 private volatile static SingLeton04 INSTANCE;
总结
本篇文章主要描述懒汉式单例的3个坑
如果懒汉式单例提供获取实例的方法没有使用同步手段那么并发请求会破坏单例,因此需要加上同步手段
如果使用同步手段(加锁)粒度太大,会导致性能问题,因此可以使用双重检测锁
由于对象创建的过程可能会进行指令重排序,如果对象发生逃逸会造成一致性问题,因此需要使用volatile禁止指令重排序