网站首页 > 技术文章 正文
考虑到马上到来的金九银十的面试季,我给大家介绍一下面试官的必问题,先从单例模式开始,这个设计模式看似简单,想回答得让面试官眼前一亮,还不是那么容易的。
一、什么是单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。
单例模式的特点:
1.类构造器私有
2.持有自己类的引用
3.对外提供获取实例的静态方法
我们先用一个简单示例了解一下单例模式的用法。
public class SimpleSingleton {
//持有自己类的引用
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
//私有的构造方法
private SimpleSingleton() {
}
//对外提供获取实例的静态方法
public static SimpleSingleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
System.out.println(SimpleSingleton.getInstance().hashCode());
System.out.println(SimpleSingleton.getInstance().hashCode());
}
}
打印结果:
1639705018
1639705018
我们看到两次获取SimpleSingleton实例的hashCode是一样的,说明两次调用获取到的是同一个对象。
可能很多朋友平时工作当中都是这么用的,但是我要说的是这段代码其实是有问题的。
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?
这个时候你也许会想到,如果在真正使用的时候再实例化不就可以了?这就是我接下来要介绍的 懒汉模式。
二、饿汉模式与懒汉模式
什么是饿汉模式?
实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。代码如下:
public class SimpleSingleton {
//持有自己类的引用
private static final SimpleSingleton INSTANCE = new SimpleSingleton();
//私有的构造方法
private SimpleSingleton() {
}
//对外提供获取实例的静态方法
public static SimpleSingleton getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
System.out.println(SimpleSingleton.getInstance().hashCode());
System.out.println(SimpleSingleton.getInstance().hashCode());
}
}
什么是懒汉模式?
顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。代码如下:
public class SimpleSingleton2 {
private static SimpleSingleton2 INSTANCE;
private SimpleSingleton2() {
}
public static SimpleSingleton2 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton2();
}
return INSTANCE;
}
public static void main(String[] args) {
System.out.println(SimpleSingleton2.getInstance().hashCode());
System.out.println(SimpleSingleton2.getInstance().hashCode());
}
}
示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。
如果代码可能有些朋友在使用,但是还是有问题。
有什么问题呢?
假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了一个INSTANCE对象的初衷。
要如何改进呢?
最简单的办法就是使用synchronized关键字,改进后的代码如下:
public class SimpleSingleton3 {
private static SimpleSingleton3 INSTANCE;
private SimpleSingleton3() {
}
public synchronized static SimpleSingleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton3();
}
return INSTANCE;
}
public static void main(String[] args) {
System.out.println(SimpleSingleton3.getInstance().hashCode());
System.out.println(SimpleSingleton3.getInstance().hashCode());
}
}
这样总可以了吧?
不好意思,还是有问题。
有什么问题?
使用synchronized关键字会消耗性能,我们应该判断INSTANCE为空时才加锁,而不为空不应该加锁,需要直接返回。这就需要使用双重检查锁。
饿汉模式 和 懒汉模式 各有什么优缺点?
饿汉模式:好处是没有线程安全的问题,坏处是浪费内存空间。
懒汉模式:好处是没有内存空间浪费的问题,但是控制不好实际不是单例。
三、双重检查锁
双重检查锁顾名思义会检查两次,在加锁之前检查一次是否为空,加锁之后再检查一次是否为空。代码如下:
public class SimpleSingleton4 {
private static SimpleSingleton4 INSTANCE;
private SimpleSingleton4() {
}
public static SimpleSingleton4 getInstance() { if (INSTANCE == null) { synchronized (SimpleSingleton4.class) { if (INSTANCE == null) { INSTANCE = new SimpleSingleton4(); } } } return INSTANCE; }
public static void main(String[] args) { System.out.println(SimpleSingleton4.getInstance().hashCode()); System.out.println(SimpleSingleton4.getInstance().hashCode()); }}
在加锁之前判断是否为空,可以确保INSTANCE不为空的情况下,不用加锁,可以直接返回。
为什么在加锁之后,还需要判断INSTANCE是否为空呢?
其实,是为了防止在多线程并发的情况下,比如:线程a 和 线程b同时调用
getInstance,同时判断INSTANCE为空,则同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次INSTANCE是否为空,则可能会重复创建实例。
不要以为这样就完了,还有问题呢?
有啥问题?
public static SimpleSingleton4 getInstance() {
if (INSTANCE == null) {//1
synchronized (SimpleSingleton4.class) {//2
if (INSTANCE == null) {//3
INSTANCE = new SimpleSingleton4();//4
}
}
}
return INSTANCE;//5
}
getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。但是java虚拟机实际上会有一些优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:
public static SimpleSingleton4 getInstance() {
if (INSTANCE == null) {//1
if (INSTANCE == null) {//3
synchronized (SimpleSingleton4.class) {//2
INSTANCE = new SimpleSingleton4();//4
}
}
}
return INSTANCE;//5
}
}
原来如此,那有什么办法可以解决呢?
可以在定义INSTANCE是加上volatile关键字,代码如下:
public class SimpleSingleton7 {
private volatile static SimpleSingleton7 INSTANCE;
private SimpleSingleton7() {
}
public static SimpleSingleton7 getInstance() {
if (INSTANCE == null) {
synchronized (SimpleSingleton7.class) {
if (INSTANCE == null) {
INSTANCE = new SimpleSingleton7();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
System.out.println(SimpleSingleton7.getInstance().hashCode());
System.out.println(SimpleSingleton7.getInstance().hashCode());
}
}
volatile 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排。
双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
除了上面的单例模式之外,还有没有其他的单例模式?
四、静态内部类
静态内部类顾名思义是通过静态的内部类来实现单例模式的。
public class SimpleSingleton5 {
private SimpleSingleton5() {
}
public static SimpleSingleton5 getInstance() {
return Inner.INSTANCE;
}
private static class Inner {
private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
}
public static void main(String[] args) {
System.out.println(SimpleSingleton5.getInstance().hashCode());
System.out.println(SimpleSingleton5.getInstance().hashCode());
}
}
我们看到在SimpleSingleton5类中定义了一个静态的内部类Inner,SimpleSingleton5类的getInstance方法返回的是内部类Inner的实例INSTANCE。
只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化INSTANCE ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。
五、枚举
枚举是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。
public enum SimpleSingleton6 {
INSTANCE;
public void doSameThing() {
}
}
但是实际情况下,枚举的单例用的并不多,因为它不好理解。
六、总结
本文主要介绍了:
饿汉模式、懒汉模式、双重检查锁、静态内部类 和 枚举 这5种单例模式,各有优缺点,静态内部类是所有单例模式中最推荐的模式。
如果您看了这篇文章觉得有所收获,帮忙关注一下我的公众账号:苏三说技术。原创不易,你们的鼓励是我坚持写作最大的动力,谢谢大家。
猜你喜欢
- 2024-09-18 JVM系列之:对象的锁状态和同步(对象锁和方法锁)
- 2024-09-18 心理测试:四个卫生间你会选择哪一个,测出你是一个怎样的人!
- 2024-09-18 用这个方法,发现你命中注定的另一半
- 2024-09-18 Mybatis为什么查询结果为空时返回值为NULL或空集合?
- 2024-09-18 ES6对象拦截器 Proxy(electron拦截请求)
- 2024-09-18 求教!CAD多段线在布局视口中变成了“空心”,怎么办
- 2024-09-18 张明楷刑法笔记(13-15)结果、因果关系、结果归属
- 2024-09-18 Mybatis查询结果为空时,为什么返回值为NULL或空集合?
- 2024-09-18 一个单相思的女孩,写给暗恋对象的信
- 2024-09-18 牧童笑称用指针,Golang入门教程,类型指针(Pointer)的使用EP05
- 1514℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 562℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 507℃MySQL service启动脚本浅析(r12笔记第59天)
- 486℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 484℃启用MySQL查询缓存(mysql8.0查询缓存)
- 464℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 444℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 441℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)