哈啰,各位小伙伴们,这里是每天进步一点点的花栗鼠小K
“前几天叛逆了一下,发生了点小插曲,没有续更设计模式,反而聊起了反射。今天呢,小K接着聊设计模式。之前聊了那么多设计模式,小K后知后觉,发现有个面试中自己经常聊的铁汁被漏掉了。它真的是面试中的万金油,聊啥都能扯到它,设计模式、bean、静态内部类……”
亮个相吧,小宝贝儿。接下来有请潘周,呃, 单例(Singleton)模式
。
01
—
什么是单例模式
简言之
单例(Singleton)模式
属于创建型模式,是指一个类只有一个实例,且该类能自行创建这个实例的一种模式
其实生活中有很多地方使用到了单例模式。举个🌰,Windows
中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
在计算机系统中,还有 Windows
的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
其 UML
如图
单例模式 UML
单例模式包含两个角色
Singleton
:单例类,包含一个实例且能自行创建这个实例的类
Client
:访问类,使用单例的类
瞅一眼这个 UML
图,大部分小伙伴都会心一笑,单例模式太TIMI简单了。感觉 Client
作为一个角色都是用来撑场子的,有点子牵强。
02
—
单例模式实现方式
实现单例模式,有一个通用的模板范式,一般需要满足以下条件
私有化构造方法,避免外部类通过 new 创建对象 定义一个私有的静态变量持有自己的类型 对外提供一个静态的公共方法来获取实例 如果实现了序列化接口需要保证反序列化不会重新创建对象
在此基础上,单例模式有五种实现方式,依次为: 饿汉式
、 懒汉式
、 双重检查锁
、静态内部类
、 枚举
小K将带领大家,依次揭秘这五种实现方式
1. 饿汉式
饿汉式,顾名思义,类一加载就创建对象,这种方式比较常用,但容易产生垃圾对象,浪费内存空间。
Singleton
代码如下
public class Singleton {
// 定义私有静态变量持有自己的类型
private static final Singleton instance = new Singleton();
// 私有化构造方法
private Singleton(){}
// 提供对外静态公共方法
public static Singleton getInstance() {
return instance;
}
}
饿汉式的优缺点如下:
优点:线程安全,没有加锁,执行效率较高
缺点:不是懒加载,类加载时就初始化,浪费内存空间懒汉式
这里提一嘴,懒加载(lazy loading)
指使用时再创建对象的加载方式
饿汉式虽然线程安全,但是可以使用反射破坏该单例
测试代码如下:
public static void main(String[] args) throws Exception {
Class singletonClass = Class.forName("designPattern.singletonPattern.eager.Singleton");
Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
System.out.println("使用反射创建的饿汉式单例:" + o);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("多线程创建的饿汉式单例:" + Singleton.getInstance());
}).start();
}
}
结果如下:
使用反射创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@1b6d3586
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e
多线程创建的饿汉式单例:designPattern.singletonPattern.eager.Singleton@6218fb2e
可见饿汉式单例虽然保证了线程安全,却防止不了反射
2. 懒汉式
懒汉式,在第一次调用时,对其进行实例化
Singleton
代码如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 判断为null 再创建对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
最原始的懒汉式单例是线程不安全的
测试如下:
public static void main(String[] args) {
for (int i=0; i<3; i++){
new Thread(()->{
System.out.println("多线程创建的单例模式:" + Singleton.getInstance());
}).start();
}
}
结果如下:
多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@54437fbd
多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@72679c71
多线程创建的单例模式:designPattern.singletonPattern.lazy.Singleton@79a2688b
懒汉式的优缺点如下:
优点:懒加载
缺点:线程不安全
因此在使用时,可以对getInstance()
方法加synchronized
关键字
public static synchronized Singleton getInstance() {}
但代价是,每一次调用 getInstance()
获取实例时都需要加锁和释放锁,这样是非常影响性能的。
添加synchronized
关键字的懒汉式优缺点如下:
优点:懒加载,线程安全
缺点:效率较低
嗐,果然人生就是,只有失去点什么,才能得到别的东西啊
吐槽结束,进入下一趴
3. 双重检查锁
双重检查锁(即
DCL
,double-checked locking
)为了在多线程环境下,不影响程序的性能,不让线程每次调用getInstance()
方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在
饿汉式和懒汉式的单例都有缺点,双重检测的实现方式解决了这两者的缺点。
双重检测将懒汉式中的 synchronized
方法改成了 synchronized
代码块。
Singleton
代码如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一层检查instance是否为null
if (instance == null) {
// synchronized加锁 注意这里是类级别的锁 先判断实例是否存在,若不存在再对类对象进行加锁处理
synchronized (Singleton.class) {
// 第二层检查instance是否为null 这里的检测避免多线程并发时多次创建对象
if (instance == null)
// new关键字创建对象不是原子操作
instance = new Singleton();
}
}
}
return instance;
}
}
测试代码同懒汉式部分的测试代码,此处不赘述
结果如下:
多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd
多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd
多线程创建的双重检查锁单例:designPattern.singletonPattern.doubleChecked.Singleton@54437fbd
可见多了些代码,效果是好一些哈
双重检查锁的优缺点如下:
优点:对象的创建是线程安全的,支持延时加载,获取对象时不需要加锁
缺点:实现较复杂
这里细说一下双重检查的两次非空判断哈
第一重判断,如果实例已经存在,就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块,同步块是为了防止有多个线程同时调用时,导致生成多个实例,有了同步块,每次只能有一个线程调用访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例 第二重空判断,当多个线程一起到达锁位置时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则为 null,会进行单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象
最关键的一个点就是 volatile
关键字的使用,双重检查锁中使用 volatile
的两个重要特性:可见性
、 禁止指令重排序
,此处不再赘述。
4. 静态内部类
用静态内部类的方式实现单例类,利用了Java 静态内部类的特性。Java 加载外部类的时候,不会创建内部类的实例,只有在外部类使用到内部类的时候才会创建内部类实例
Singleton
的代码如下:
public class Singleton {
private Singleton() {}
// 定义静态内部类
private static class InnerClass{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InnerClass.INSTANCE;
}
}
这时候,一个小K前面一直在关注的问题,此时也要重视一下:静态内部类实现方式线程安全吗?
测试代码同懒汉式部分的测试代码,此处不赘述
来看结果:
多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71
多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71
多线程创建的静态内部类单例:designPattern.singletonPattern.static.Singleton@72679c71
可见,这一方式也是线程安全的
静态内部类的优缺点如下:
优点:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵),效率较高,实现简单 缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象
5. 枚举
枚举实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性
Singleton
代码如下:
public enum Singleton {
INSTANCE;
}
枚举在Java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例,可以直接通过调用获取实例
Singleton instance = Singleton.INSTANCE;
枚举方式是线程安全的,且可以避免反射的破坏
测试代码如下:
public static void main(String[] args) throws Exception{
// 线程安全测试
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("多线程创建的枚举单例:" + designPattern.singletonPattern.static.Singleton.getInstance());
}).start();
}
// 反射测试
Class singletonClass = Class.forName("designPattern.singletonPattern.enmu.Singleton");
Constructor declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
System.out.println("使用反射创建的枚举单例:" + o);
}
结果如下:
多线程创建的枚举单例:INSTANCE
多线程创建的枚举单例:INSTANCE
多线程创建的枚举单例:INSTANCE
Exception in thread "main" java.lang.NoSuchMethodException: designPattern.singletonPattern.enmu.Singleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at designPattern.singletonPattern.enmu.Client.main(Client.java:17)
运行结果报错,所以无法通过反射创建枚举的实例,而枚举类自身特性也保证了线程安全
个人体悟
实现方式 | 是否线程安全 | 是否可被反射破坏 |
---|---|---|
饿汉式 | 否 | 是 |
懒汉式 | 是 | 是 |
双重检查锁 | 是 | 是 |
静态内部类 | 是 | 否 |
枚举 | 是 | 否 |
就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例
03
—
单例模式的优缺点
前面说了每种实现方式的优缺点,那么整体看单例模式,其优缺点可以概括如下:
优点:
单例模式可以保证内存里只有一个实例,减少了内存的开销 可以避免对资源的多重占用 单例模式设置全局访问点,可以优化和共享资源的访问 缺点:
单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则
04
—
总结
本期介绍的单例模式是面试中经常会被问到的一个问题,网上有大量的文章介绍单例模式的实现,本文也是参考那些优秀的文章来做一个总结,通过自己在学习过程中的理解进行记录,并补充完善一些内容,一方面巩固自己所学的内容,另一方面希望能对其他同学提供一些帮助
本期就到这了,这里是花栗鼠小K,下次有🌰,我再来,拜拜~~~
关注六只栗子,面试不迷路!
作者 花栗鼠小K
编辑 一口栗子
原文始发于微信公众号(六只栗子):Java面试万金油,是单例吖
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88526.html