单例模式(史上最全)

文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《尼恩Java面试宝典 最新版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


该如何优雅的、安全的使用单例模式呢?

单例模式是Java的核心模式,最好人人都精通。

那么,该如何优雅的使用单例模式呢?

来看看:

  • 缓存之王 Caffeine 源码中,如何使用单例模式的?
  • 链路之王 Skywalking 源码中,如何使用单例模式的?

另外,也看看 美团是如何进行 单例模式 的面试的。下面是一个美团面试题:

  • 单例模式懒汉式和饿汉式有哪些区别?(美团)

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从下面的链接获取:语雀 或者 码云

1.什么是单例

  • 保证一个类只有一个实例,并且提供一个访问该全局访问点

2.那些地方用到了单例模式

  1. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  2. 应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
  3. 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
  4. Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
  5. windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。

3.单例优缺点

优点:

  1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
  2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
  3. 提供了对唯一实例的受控访问。
  4. 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
  5. 允许可变数目的实例。
  6. 避免对共享资源的多重占用。

缺点:

  1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  3. 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  4. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

4.单例模式使用注意事项:

  1. 使用时不能用反射模式创建单例,否则会实例化一个新的对象
  2. 使用懒单例模式时注意线程安全问题
  3. 饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)

5.单例防止反射漏洞攻击

private static boolean flag = false;

private Singleton() {

	if (flag == false) {
		flag = !flag;
	} else {
		throw new RuntimeException("单例模式被侵犯!");
	}
}

public static void main(String[] args) {

}

6.如何选择单例创建方式

  • 如果不需要延迟加载单例,可以使用枚举或者饿汉式,相对来说枚举性好于饿汉式。
    如果需要延迟加载,可以使用静态内部类或者懒汉式,相对来说静态内部类好于懒韩式。
    最好使用饿汉式

7.单例创建方式

(主要使用懒汉和懒汉式)

1.饿汉式:
类初始化时,会立即加载该对象,线程天生安全,调用效率高。

2.懒汉式:
类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

3.静态内部方式:
结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

4.枚举单例:
使用枚举实现单例模式

优点: 实现简单、调用效率高,枚举本身就是单例, 由jvm从根本上提供保障!避免通过反射和反序列化的漏洞;

缺点: 没有延迟加载。

5.双重检测锁方式

因为JVM重排序、内存可见性的原因,可能会初始化多次,

所以: 需要通过 Double Check 双重检查+ synchronized + Volatile 解决 同步问题和可见性问题。

1.饿汉式

类初始化时,会立即加载该对象,线程天生安全,调用效率高。

package com.crazymakercircle.designmodel.singleton;
//饿汉式
public class FSingleton {

    // 类初始化时,会立即加载该对象,线程安全,调用效率高
    private static final FSingleton instance = new FSingleton();

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

 public   static FSingleton getInstance() {
        return instance;
    }


}


饿汉模式就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。

特点:

  • 是否 Lazy 初始化:否
  • 是否多线程安全:是
  • 实现难度:易

优点:

  • 没有加锁,执行效率会提高。
  • 这种方式比较常用,但容易产生垃圾对象
  • 它基于JVM class loader 机制, 是单线程执行的, 避免了多线程的同步问题

缺点:

  • 类加载时就初始化,浪费内存,

2.懒汉式

类初始化时,不会初始化该对象,

真正需要使用的时候,才会创建该对象,具备懒加载功能。

package com.crazymakercircle.designmodel.singleton;
//懒汉模式
public class FLazySingleton {

    //类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
    private static  FLazySingleton instance = null;

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

    //真正需要使用的时候才会创建该对象
    public static synchronized FLazySingleton getInstance() {
        if(null==instance)
        {
            instance=new FLazySingleton();
        }
        return instance;
    }
}

3.静态内部类

静态内部方式:

结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

package com.crazymakercircle.designmodel.singleton;

public class Singleton { 
    //静态内部类 
    private static class LazyHolder { 
          //通过final保障初始化时的线程安全  
           private static final Singleton INSTANCE = new Singleton(); 
    } 
       //私有的构造器 
    private Singleton (){} 
      //获取单例的方法 
    public static final Singleton getInstance() { 
      //返回内部类的静态、最终成员 
       return LazyHolder.INSTANCE; 
    } 
} 

4.枚举单例式

枚举单例:

使用枚举实现单例模式 优点:实现简单、调用效率高,

枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。

package com.lijie;

package com.crazymakercircle.designmodel.singleton;
//饿汉式
public enum SingletonEnumStyle {
    INSTANCE;
    // 类初始化时,会立即加载该对象,线程安全,调用效率高

    public  static SingletonEnumStyle getInstance() {
        return INSTANCE;
    }

}

枚举实现单例模式 优点:

  • 实现简单、枚举本身就是单例,由jvm从根本上提供保障!
  • 避免通过反射和反序列化的漏洞

缺点:

  • 没有延迟加载

5.双重检测锁方式

所谓懒加载,就是直到第一次被调用时才加载。其实现需要考虑并发问题和指令重排,代码如下:

public class Singleton {
 
    private volatile static Singleton instance; //①
 
    private Singleton() { //②
    }
 
    public static Singleton getInstance() {
        if (instance == null) {//③
            synchronized (Singleton.class) {
                if (instance == null) {//④
                    instance = new Singleton();//⑤
                }
            }
        }
        return instance;
    }
}

这段代码精简至极,没有一个字符是多余的,下面逐行解读一下:

首先,注意到①处的volatile关键字,它具备两项特性:

一是保证此变量对于所有线程的可见性。

即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

二是禁止指令重排序优化。

这里解释一下指令重排序优化:

代码 ⑤ 处的instance = new Singleton(); 并不是原子的,大体可分为如下 3 步:

  1. 分配内存
  2. 调用构造函数初始化成实例
  3. 让instance指向分配的内存空间

JVM 允许在保证结果正确的前提下进行指令重排序优化。

即如上 3 步可能的顺序为1->2->3 或 1->3->2 。

如果顺序是 1->3->2 ,当 3 执行完,2 还未执行时,另一个线程执行到代码 ③ 处,发现instance不为null,直接返回还未初始化好的instance并使用,就会报错。

所以使用volatile,就是为了保证线程间的可见性和防止指令重排。

其次,代码②处将构造函数声明为private目的:在于阻止使用new Singleton()这样的代码生成新实例。

最后,当客户端调用Singleton.getInstance()时,先检查是否已经实例化(代码③),未实例化时同步代码块,然后再次检查是否已实例化(代码④),然后才执行代码⑤。

两次检查的意义在于,防止synchronized同步过程中其他线程进行了实例化。

这就是著名的双重检查锁(Double check lock)实现单例,也即懒加载。

TIPS:

网上也有直接对 getInstance()方法加锁的版本,这样大范围的方法级别加锁会导致并发变低,实际上第一次调用生成实例之后,后续获取实例根本不需要并发控制了。

而本例的双重检查锁版本可以避免此并发问题。

双重检测锁 单例 非常重要, 涉及到Volatile 和可见性的底层原理, 深入学习/系统学习 双重检测锁 单例的内容, 请参见 《Java 高并发核心编程 卷2》 第8.1节:线程安全的单例模式

单例模式(史上最全)_第1张图片

缓存之王 Caffeine 源码中,如何使用单例模式的?

答案是:枚举单例

并且,单例的名称叫做 INSTANCE

通过这个 INSTANCE 名字 做 关键词搜索, 能搜到一大把

单例模式(史上最全)_第2张图片

来一个案例

单例模式(史上最全)_第3张图片

再来一个案例

单例模式(史上最全)_第4张图片

缓存之王 Caffeine 的详细资料,请参考下面的博客、或者对应的PDF文件:

  • 《彻底穿透 缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
  • 《彻底穿透 缓存之王:Caffeine 的使用(史上最全)》

链路之王 Skywalking 源码中,如何使用单例模式的?

答案是:枚举单例

并且,单例的名称叫做 INSTANCE

单例模式(史上最全)_第5张图片

8 单例模式懒汉式和饿汉式有哪些区别?(美团)

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

注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

明确定义后,看一下代码:

饿汉模式

package com.crazymakercircle.designmodel.singleton;
//饿汉式
public class FSingleton {

    // 类初始化时,会立即加载该对象,线程安全,调用效率高
    private static final FSingleton instance = new FSingleton();

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

 public   static FSingleton getInstance() {
        return instance;
    }
}

饿汉模式就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。

特点:

  • 是否 Lazy 初始化:否
  • 是否多线程安全:是
  • 实现难度:易

优点:

  • 没有加锁,执行效率会提高。
  • 这种方式比较常用,但容易产生垃圾对象
  • 它基于JVM class loader 机制, 是单线程执行的, 避免了多线程的同步问题

缺点:

  • 类加载时就初始化,浪费内存,

懒汉模式

public class Singleton {
 
    private volatile static Singleton instance; //①
 
    private Singleton() { //②
    }
 
    public static Singleton getInstance() {
        if (instance == null) {//③
            synchronized (Singleton.class) {
                if (instance == null) {//④
                    instance = new Singleton();//⑤
                }
            }
        }
        return instance;
    }
}

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

特点:

  • 是否 Lazy 初始化:是
  • 是否多线程安全:是
  • 实现难度:难

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,需要通过多种手段,保证线程安全和内存可见性:

  • volatile 保证内存可见性
  • synchronized + 双重检查 保证线程安全

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

  • 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 主要解决:一个全局使用的类频繁地创建与销毁。
  • 何时使用:当您想控制实例数目,节省系统资源的时候。
  • 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码:构造函数是私有的。
  • 应用实例:

1、一个党只能有一个主席。
2、Windows是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用 Double Check 双重检查锁,synchronized (Singleton.class) 防止多线程同时进入造成instance 被多次实例化。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从下面的链接获取:语雀 或者 码云

推荐阅读:

  • 《尼恩Java面试宝典》

  • 《Springcloud gateway 底层原理、核心实战 (史上最全)》

  • 《Flux、Mono、Reactor 实战(史上最全)》

  • 《sentinel (史上最全)》

  • 《Nacos (史上最全)》

  • 《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》

  • 《clickhouse 超底层原理 + 高可用实操 (史上最全)》

  • 《redis 集群 实操 (史上最全、5w字长文)》

  • 《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》

  • 《红黑树( 图解 + 秒懂 + 史上最全)》

  • 《分布式事务 (秒懂)》

  • 《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》

  • 《缓存之王:Caffeine 的使用(史上最全)》

  • 《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》

  • 《Docker原理(图解+秒懂+史上最全)》

  • 《Redis分布式锁(图解 - 秒懂 - 史上最全)》

  • 《Zookeeper 分布式锁 - 图解 - 秒懂》

  • 《Zookeeper Curator 事件监听 - 10分钟看懂》

  • 《Netty 粘包 拆包 | 史上最全解读》

  • 《Netty 100万级高并发服务器配置》

  • 《Springcloud 高并发 配置 (一文全懂)》

你可能感兴趣的:(java,单例模式,java,面试)