单例模式及其单线程环境下的经典实现(一)_

本文首先概述了单例模式的动机,并揭示了单例模式的本质和应用场景。接下来,我们给出单线程环境下单例模式的两个经典实现:饥饿和惰性,但是饥饿是线程安全的,而惰性不是线程安全的。

面对“金三”,你即将过关,还没有找到心仪的工作,本文准备了一份阿里的面试题和面试心得,希望对你有所帮助。

单例模式概述

单例模式(Singleton),又称单子模式,是一种常用的设计模式。应用此模式时,单例对象的类必须确保仅存在一个实例。在很多情况下,整个系统只需要一个全局对象,这有助于我们协调整个系统的行为。例如,在一个服务器程序中,服务器的配置信息存储在一个文件中。这些配置数据由单例对象统一读取,然后服务进程中的其他对象通过单例对象获取配置信息。显然,这简化了复杂环境中的配置管理。

特别是在计算机系统中,线程池、缓存、日志对象、对话框、打印机和显卡的驱动对象经常被设计成单例。事实上,这些应用程序或多或少都具有资源管理器的功能。例如,每台计算机可以有多台打印机,但只有一台 Printer Spooler(单例),以避免两个打印作业同时输出到打印机。再比如,每台计算机可以有多个通信端口,系统应该集中管理这些通信端口(单例),避免两个请求同时调用一个通信端口。简而言之,选择单例模式是为了避免不一致的状态,避免政治牛市。

综上所述,单例模式是一种确保类只有一个实例并为整个系统提供全局访问点的方法。

单例模式及其在单线程环境中的经典实现

单例模式应该是 23 种设计模式中最简单的。下面从单例模式的定义、类型、结构和使用元素四个方面来介绍一下。

1、单例模式理论基础

定义:保证一个类只有一个实例,并为整个系统提供一个全局访问点(将这个实例提供给整个系统)。

类型:创作模式

结构体:

特别是为了更好的理解上面的类图,我们以此为契机,介绍一下类图的几个知识点:

三要素:

2、单线程环境下的两个经典实现

在介绍单线程环境下单例模式的两种经典实现之前,有必要先解释一下立即加载和延迟加载的概念。

在单线程环境中,根据实例化对象的时间,有两种经典的单例模式实现:

Hungry 风格的单例实例化一个对象,并在单例类加载时给它自己的引用;而惰性样式的单例实例化一个对象并仅在实际使用时将其提供给自己的引用。代码示例如下:

饥饿的单身人士:

// 饥饿的单身人士

公共类 Singleton1 {

// 一个指向自己实例的私有静态引用,主动创建

私有静态 Singleton1 singleton1 = new Singleton1();

// 私有构造函数

私有 Singleton1(){}

// 以自己的实例为返回值的静态公共方法,静态工厂方法

公共静态 Singleton1 getSingleton1(){

返回单例1;

}

}

懒惰的单身人士:

// 懒惰的单例

公共类 Singleton2 {

// 对自己实例的私有静态引用

私人静态单例2单例2;

// 私有构造函数

私有 Singleton2(){}

公共静态 Singleton2 getSingleton2(){

// 被动创建,真正需要时才创建

if (singleton2 == null) {

单例2 =新单例2();

}

返回单例2;

}

}

从惰性单例中我们可以看出,单例实例是惰性加载的,也就是说,一个对象只有在实际使用时才会被实例化并给出自己的引用。

简而言之,在速度和响应时间方面,饥饿风格(也称为立即加载)更好;在资源利用效率方面,惰性风格(也称为延迟加载)更好。

3、单例模式的优点

从单例模式的定义和实现中,我们可以知道单例模式具有以下优点:

4、单例模式使用场景

由于单例模式具有以上优点,形式上也比较简单,是日常开发中使用较多的一种设计模式。它的核心是为整个系统提供一个唯一的实例。其应用场景包括但不限于以下几种:

5、单例模式注意事项

在使用单例模式时,我们必须使用单例类提供的公共工厂方法来获取单例对象,而不是使用反射来创建,否则会实例化一个新的对象。另外,在多线程环境中使用单例模式时,要特别注意线程安全问题,下面我将重点介绍。

多线程环境下单例模式的实现

在单线程环境中,饥饿和懒惰的单例都可以正常工作。但是,在多线程环境下,情况发生了变化:因为饥饿单例本身就是线程安全的,所以可以直接在多线程中使用而没有问题;但是惰性单例本身并不是线程安全的,所以会存在多个实例,这与单例模式的初衷背道而驰。下面我将重点讨论以下问题:

1. 为什么说 Hungry 风格的单例本质上是线程安全的?2. 为什么传统的惰性单例不是线程安全的?3. 如何修改传统的惰性单例使其线程安全?4. 线程安全单例的实现是什么,如何实现?5. 双重校验模式,单例模式下Volatile关键字的应用6. 单例模式下ThreadLocal的应用

特别是为了更好的观察单例模式的实现是否是线程安全的,我们提供了一个简单的测试程序来验证。本示例程序的判断原理为:

启动多个线程分别获取singleton,然后打印他们得到的singleton的hashCode值。如果它们得到的单例是相同的(单例模式的实现是线程安全的),那么它们的hashCode值必须完全相同;如果它们的hashCode值不完全一样,那么得到的单例肯定不一样,即单例模式的实现不是线程安全的,而是多个实例。

公共类测试{

公共静态无效主要(字符串[]参数){

线程[]线程=新线程[10];

对于 (int i = 0; 我

线程[i] = new TestThread();

}

线程[i].start();

}

}

}

类 TestThread 扩展线程 {

@覆盖

公共无效运行(){

// 对于不同单例模式的实现,只需更改对应的单例类名及其公共静态工厂方法名即可

int hash = Singleton5.getSingleton5().hashCode();

System.out.println(hash);

}

}

1、为什么你说 Hungry 风格的单例本质上是线程安全的?

// 饥饿的单身人士

// 私有构造函数

}

}

我们上面已经提到,类加载的方式是按需加载的,而且只有一次。因此,在加载上述单例类时,会实例化一个对象并赋予其自己的引用以供系统使用。换句话说,单例对象是在线程访问它之前创建的。另外,由于一个类在整个生命周期中只会被加载一次,所以单例类只会创建一个实例,也就是说,线程每次只能并且必须只能获取这个唯一的对象。因此,据说饥饿的单例本质上是线程安全的。

2、为什么传统的惰性单例不是线程安全的?

// 传统的惰性单例

// 私有构造函数

if (singleton2 == null) {

}

}

}

上述非线程安全的一个明显原因是会有多个线程同时进入 if (singleton2 == null) {…} 块。发生这种情况时,单例类会创建多个实例,这与单例模式的初衷背道而驰。因此,传统的惰性单例不是线程安全的。

3、实现线程安全的惰性单例的几个正确姿势

同步延迟加载 – 同步方法

// 线程安全的惰性单例

// 使用同步修饰,同步和独占访问关键资源

公共静态同步 Singleton2 getSingleton2(){

if (singleton2 == null) {

}

}

}

这个实现和上面传统的惰性单例实现的唯一区别就是是否使用 synchronized 来修改 getSingleton2() 方法。如果使用,可以保证对关键资源的同步和独占访问,并且保证单例。

同步延迟加载 – 同步块

synchronized(Singleton2.class){ // 使用同步块,同步互斥锁访问关键资源

if (singleton2 == null) {

}

}

}

}

这个实现和上面同步方法版本的实现类似,这里不再赘述。从执行结果来看,问题已经解决了,但是这个执行的效率还是比较低的。其实和使用同步方式的版本相比,效率上基本没有提升。

同步延迟加载——使用内部类实现延迟加载

公共类 Singleton5 {

// 私有内部类,按需加载,随时间加载,即懒加载

私有静态类持有者{

私有静态 Singleton5 singleton5 = new Singleton5();

}

私人 Singleton5() {

}

公共静态 Singleton5 getSingleton5() {

返回 Holder.singleton5;

}

}

如上代码所示,我们可以使用内部类来实现线程安全的惰性单例。这种方法也是一种比较有效的方法。它和饥饿单例的区别在于,这种方法不仅是线程安全的,或者说是延迟加载,它只有在真正使用时才会被初始化。

当客户端调用 getSingleton5() 方法时,会触发 Holder 类的初始化。由于singleton5是Hold的类成员变量,当JVM调用Holder类的类构造函数对其进行初始化时,虚拟机保证了一个类的类构造函数在多线程环境中被正确锁定和同步。如果一个线程同时初始化一个类,那么只有一个线程会执行这个类的类构造函数,其他线程需要阻塞等待,直到活动线程执行该方法。这种情况下,虽然会阻塞其他线程,但是如果执行类构造方法的线程退出,其他线程在唤醒后不会再次进入/执行类构造,因为在同一个类加载器下,只会初始化一个类型一次,

阿里面试题

1. 多个线程同时读写,读线程数远大于写线程数。您认为应该如何解决并发问题?您会选择添加哪种锁?2. 你了解 JAVA 的 AQS 吗?它是做什么的?3. 除了synchronized关键字,如何保证线程安全?4. Tomcat本身的参数一般是怎么调整的?5. 你用过 Spring 的 AOP 吗?它是干什么用的?它将如何使用?6. 如果一个接口有两个不同的实现,如何自动装配一个具体的实现?7. 如果你想在一个bean生成并组装好之后执行你自己的逻辑,你该如何实现呢?8. 为什么 SpringBoot 不用放在 web 容器中就可以运行 HTTP 服务?9. 如果你想在 SpringBoot 中使用自定义配置文件而不仅仅是 application.properties,你应该怎么做?10.如果SpringMVC要将输出Object(如XXResult或XXResponse)打包成JSON输出,应该怎么做?11.如果有很多数据插入MYSQL,你会选择什么方法?12.如果查询很慢,你会想到的第一种方式是什么?索引是干什么用的?13. 查询死了,用什么命令找出执行的查询过程?发现后通常会怎么做?14. 读写分离是怎么做的?您认为中间件将如何工作?这和生意有什么关系?15. 你做过分库分表吗?在线迁移过程是怎样的?我怎样才能确定数据是正确的?16. 你知道哪些GC策略或者你在网上使用过哪些?它有哪些优势饿汉模式为什么没有线程安全问题,适用于哪些场景?17. JAVA类加载器有哪些类型?他们之间的父子关系是什么?家长委托是什么意思?有什么好处?18. 如何自定义一个类加载器?您使用过哪些或在哪里需要自定义类加载器?19.堆内存设置的参数有哪些?20.HashMap和Hashtable的区别。您知道或在线使用哪些 GC 策略?它有哪些优势,适用于哪些场景?17. JAVA类加载器有哪些类型?他们之间的父子关系是什么?家长委托是什么意思?有什么好处?18. 如何自定义一个类加载器?您使用过哪些或在哪里需要自定义类加载器?19.堆内存设置的参数有哪些?20.HashMap和Hashtable的区别。您知道或在线使用哪些 GC 策略?它有哪些优势,适用于哪些场景?17. JAVA类加载器有哪些类型?他们之间的父子关系是什么?家长委托是什么意思?有什么好处?18. 如何自定义一个类加载器?您使用过哪些或在哪里需要自定义类加载器?19.堆内存设置的参数有哪些?20.HashMap和Hashtable的区别。@9.堆内存设置的参数是什么?20.HashMap和Hashtable的区别。@9.堆内存设置的参数是什么?20.HashMap和Hashtable的区别。

21. 实现一个保证迭代顺序的HashMap。22. 说说排序算法,稳定性,复杂度。23. 谈谈GC。24.JVM如何加载一个类的过程,在父委托模型中有哪些方法?25. TCP如何保证可靠传输?三向握手过程?面试经验1. 准备要充分,知识要尽量广,深度要够。2.至于面试安排,如果不赶时间,尽量给自己多一点时间,在家呆两天,及时做总结补充。3. 你需要让自己的心态平静下来。作为技术交流,面试部分取决于你的运气,还要看一些眼力。有些面试官会觉得你只用了一张嘴就完成了面试。如果你想去的公司面试不好,不要灰心,继续努力准备。4. 简历投递方面,有很多不匹配的问题,可能是因为我的学历(自测书),对自信心有点打击。如果你也有同感,不妨换个BOSS或者其他平台。避免打击你的自信。5. 写简历的时候一定要体现出自己的强项,最好是类似的,用了什么技术,解决了什么问题。简历上写的东西一定要经过深思熟虑。6. 类似你的强项是什么,你认为你的项目有什么更好的地方,你能给公司带来什么?这样的问题你需要先想清楚,以免紧张,当场说话。不好。

以上是我整理的单例模式和多线程,以及阿里的面试题和面试心得。写得不好。请批评和纠正我。在这“金三”快要过去的时候饿汉模式为什么没有线程安全问题,希望能够回答那些还在面试的人。你可以帮忙!!

更多采访和结构视频资料:进群:571617441!!

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论