👨🎓作者:bug菌
✏️博客:CSDN、掘金、infoQ、51CTO等
🎉简介:CSDN|阿里云|华为云|51CTO等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计15w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。...
✍️温馨提醒:本文字数:1999字, 阅读完需:约 5 分钟
🏆本文收录于《Java进阶实战》,专门攻坚指数提升。
本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。
小伙伴们在批阅文章的过程中如果觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐+关注👨🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻♀️,创作不停💕,加油☘️
1. 前言
哈喽,小伙伴们,你们好呀,今天我们就不整那枯燥无味的知识点了,偶尔换换口味,我们来玩点高级的;由于很多小伙伴都在给我传递一种负面情绪,今年的工作很难找,我就在想是不是八股文没准备充足啊?于是我就在总结高频笔试题,借此想把整理到的笔试题进行集合式的讨论,不仅帮助大家理解,也能帮助自己加深理解,何乐而不为呢。
2. 环境说明
本地的开发环境:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 1.8
- Spring Boot版本:2.3.1 RELEASE
- Maven版本:3.8.2
3. 需求分析
手写一个单例?那么首先你就要清楚单例是什么?如果都不清楚何为单例,那无法下手!首先我就带着大家简单回顾一下设计模式之单例模式的相关知识点吧。
3.1 概念
单例模式是java中设计模式里最简单的模式之一,属于创建型模式。这种模式它提供了一种创建对象的最佳方式,在创建对象的过程中,只涉及一个类,且该类负责创建自身对象,并保证只能创建单个对象实例。
总而言之,该模式的主要目的就是确保一个类只能有一个实例存在。
3.2 特点
单例模式特点可总结为以下3点
1、单例类只有一个实例对象。
2、该实例对象必须由单例类本身自行创建。
3、单例类必须提供一个共有的静态方法向外面提供这个实例。
3.3 分类
单例模式可根据实例创建的时机进行分类,可分为[饿汉式、懒汉式]两类。
饿汉式:类加载就会创建单实例对象。
懒汉式:类加载不会导致该单实例对象被创建,只有在被使用的时候才会创建。
总而言之,分类的意义在于实际运用场景决定,各有各的优缺点。比如懒汉模式,只有在被使用的时候才创建,节省资源,体现了延迟加载的思想;但在并发场景下同一时间被多个线程调用,则很有可能被创建出多个实例,违背唯一实例原则。而恶汉模式,写法上简单,而且在多线程下也能保证唯一实例,但如果外部一直未调用该实例又先实例化了,这部分资源也就白白浪费了。
4. 代码演示
如下我将分别从单例模式的两种分类来实现代码单例,仅供参考:
4.1 懒汉式单例(线程安全)
代码实现如下:
public class LazySingLeton {
//私有构造方法
private LazySingLeton() {
System.out.println("生成LazySingLeton实例一次");
}
//在成员位置创建该类的对象
private static LazySingLeton instance = null;
public synchronized static LazySingLeton getInstance() {
if (instance == null) {
instance = new LazySingLeton();
}
return instance;
}
}
注意:此种写法虽然解决了线程安全问题,但synchronized同步的方法是静态的,会导致进入该方法时JVM会锁定LazySingLeton这个类,锁的粒度太大,大量线程同时访问的时候直接阻塞,性能太低。
接下来,我们还是先写个main函数顺序调用上三遍懒汉式单例测试一波看看。
//测试
public static void main(String[] args) {
LazySingLeton.getInstance();
LazySingLeton.getInstance();
LazySingLeton.getInstance();
}
大家可以看到,实例确实只被创建了一次!而非三次。
4.2 恶汉式单例
代码实现如下:
public class NoLazySingLeton {
// 私有构造方法
private NoLazySingLeton() {
System.out.println("生成NoLazySingLeton实例一次!");
}
// 在成员位置创建该类的对象
private static NoLazySingLeton instance = new NoLazySingLeton();
// 对外提供静态方法获取该实例对象
public static NoLazySingLeton getInstance() {
return instance;
}
}
写个main函数顺序调用上三遍恶汉式单例,静待结果:
//测试
public static void main(String[] args) {
//调用三遍
NoLazySingLeton.getInstance();
NoLazySingLeton.getInstance();
NoLazySingLeton.getInstance();
}
大家可以看到,实例确实只被创建了一次!而非三次。
对比以上两种写法,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成线程不安全就完全不存在了,这也就是懒汉恶汉式的抉择问题了,重点也就在于你是何应用场景了。
4.3 懒汉式单例(双重检查模式)
最后,我们再来讨论一下,对比上方保证线程安全懒汉式而言,若想做到即解决性能问题又能保证线程安全,那可以这么干,浅浅听我分析,对于 getInstance() 方法来说,绝大部分的操作都是读 操作,读操作是线程安全的,所以没必让每个线程必须持有锁才能调用该方法,我们只需要调整加锁的时机,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,从而达到解决性能消耗的问题。具体代码大家请看如下,我把代码注释都写的明明白白。
public class DoubleLazySingLeton {
private static DoubleLazySingLeton instance;
// 私有构造方法
private DoubleLazySingLeton() {
System.out.println("生成DoubleLazySingLeton实例一次!");
}
// 对外提供静态方法获取该对象
public static DoubleLazySingLeton getInstance() {
// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例.
if (instance == null) {
//instance未实例化的时候才加锁
synchronized (DoubleLazySingLeton.class) {
// 抢到锁之后再次判断是否为null
if (instance == null) {
instance = new DoubleLazySingLeton();
}
}
}
return instance;
}
}
但是话又说回来, 虽然【双重检查模式】能保证性能及线程安全,但是也并非完美无缺,在多线程情况下,可能会有空指针异常的问题,因为JVM在实例化对象的时候会进行优化和指令重排序操作。
若要解决双重检查模式带来空指针异常的问题,你只需要使用[volatile]关键字,因为volatile关键字可以保证可见性和有序性(禁止指令重排序),顾保证了new DoubleLazySingLeton()创建对象实例化过程的顺序性,具体请看如下:
4.4 懒汉式单例(DCL双重校验锁)
具体代码如下:
public class DoubleLazySingLeton {
//关键字volatile保证对象实例化过程的顺序性。
private static volatile DoubleLazySingLeton instance;
// 私有构造方法
private DoubleLazySingLeton() {
System.out.println("生成DoubleLazySingLeton实例一次!");
}
// 对外提供静态方法获取该对象
public static DoubleLazySingLeton getInstance() {
// 第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例.
if (instance == null) {
//instance未实例化的时候才加锁
synchronized (DoubleLazySingLeton.class) {
// 抢到锁之后再次判断是否为null
if (instance == null) {
instance = new DoubleLazySingLeton();
}
}
}
return instance;
}
}
由于volatile关键字它可以禁止指令重排序来保证一定的有序性,自然就解决了空指针异常的问题。
5. 热文推荐🔥
- 浅谈你对单例类中使用volatile关键字的理解
- Mysql分页 vs Oracle分页 对比分析
- Java 如何实现手动连接数据库(MySQL或Oracle)?
- Java 如何实现获取客户端公网IP地址?
- 为什么print和println输出java对象时会打印内存地址?
- 如何解决springboot拦截器@Autowried注入为空问题?
- MyBatis中的discriminator鉴别器如何使用?
- 如何快速手撕单例类?一文教会你
- 如何保证三个线程按顺序执行?不懂我教你
- 简谈你对synchronized关键字的使用
6. 最后🔥
🏆本文收录于《Java进阶实战》,需要的小伙伴可以进去瞅瞅。
本专栏致力打造最硬核Java进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。
我是bug菌,一名CSDN / 阿里云 / 华为云 / 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者,全网粉丝合计10w+,硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!一起学习,一起变强。
关注公众号,获取最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等硬核资源