详细理解单例模式与多线程+阿里面试题+面试心得

本文首先概述了单例模式产生动机,揭示了单例模式的本质和应用场景。紧接着,我们给出了单例模式在单线程环境下的两种经典实现:饿汉式懒汉式,但是饿汉式是线程安全的,而懒汉式是非线程安全的。

详细理解单例模式与多线程+阿里面试题+面试心得

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

单例模式概述

详细理解单例模式与多线程+阿里面试题+面试心得

单例模式(Singleton),也叫单子模式,是一种常用的设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候,整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,显然,这种方式简化了在复杂环境下的配置管理。

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

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

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

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

1、单例模式理论基础

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

类型: 创建型模式

结构

详细理解单例模式与多线程+阿里面试题+面试心得

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

  • 类图分为三部分,依次是类名、属性、方法;
  • 以<<开头和以>>结尾的为注释信息;
  • 修饰符+代表public,-代表private,#代表protected,什么都没有代表包可见;
  • 带下划线的属性或方法代表是静态的。

三要素

  • 私有的构造方法;
  • 指向自己实例的私有静态引用;
  • 以自己实例为返回值的静态的公有方法。

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

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

  • 立即加载 : 在类加载初始化的时候就主动创建实例;
  • 延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

在单线程环境下,单例模式根据实例化对象时机的不同,有两种经典的实现:一种是 饿汉式单例(立即加载),一种是 懒汉式单例(延迟加载)。饿汉式单例在单例类被加载时候,就实例化一个对象并交给自己的引用;而懒汉式单例只有在真正使用的时候才会实例化一个对象并交给自己的引用。代码示例分别如下:

饿汉式单例

 // 饿汉式单例
public class Singleton1 {
 // 指向自己实例的私有静态引用,主动创建
 private static Singleton1 singleton1 = new Singleton1();
 // 私有的构造方法
 private Singleton1(){}
 // 以自己实例为返回值的静态的公有方法,静态工厂方法
 public static Singleton1 getSingleton1(){
 return singleton1;
 }
} 

我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

懒汉式单例

// 懒汉式单例
public class Singleton2 {
 // 指向自己实例的私有静态引用
 private static Singleton2 singleton2;
 // 私有的构造方法
 private Singleton2(){}
 // 以自己实例为返回值的静态的公有方法,静态工厂方法
 public static Singleton2 getSingleton2(){
 // 被动创建,在真正需要使用时才去创建
 if (singleton2 == null) {
 singleton2 = new Singleton2();
 }
 return singleton2;
 }
}

我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

总之,从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些

3、单例模式的优点

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

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

4、单例模式的使用场景

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

  • 有状态的工具类对象;
  • 频繁访问数据库或文件的对象;

5、单例模式的注意事项

在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象。此外,在多线程环境下使用单例模式时,应特别注意线程安全问题,我在下文会重点讲到这一点。

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

在单线程环境下,无论是饿汉式单例还是懒汉式单例,它们都能够正常工作。但是,在多线程环境下,情形就发生了变化:由于饿汉式单例天生就是线程安全的,可以直接用于多线程而不会出现问题;但懒汉式单例本身是非线程安全的,因此就会出现多个实例的情况,与单例模式的初衷是相背离的。下面我重点阐述以下几个问题:

  1. 为什么说饿汉式单例天生就是线程安全的?
  2. 传统的懒汉式单例为什么是非线程安全的?
  3. 怎么修改传统的懒汉式单例,使其线程变得安全?
  4. 线程安全的单例的实现还有哪些,怎么实现?
  5. 双重检查模式、Volatile关键字 在单例模式中的应用

ThreadLocal 在单例模式中的应用

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

开启多个线程来分别获取单例,然后打印它们所获取到的单例的hashCode值。若它们获取的单例是相同的(该单例模式的实现是线程安全的),那么它们的hashCode值一定完全一致;若它们的hashCode值不完全一致,那么获取的单例必定不是同一个,即该单例模式的实现不是线程安全的,是多例的。

public class Test {
 public static void main(String[] args) {
 Thread[] threads = new Thread[10];
 for (int i = 0; i < threads.length; i++) {
 threads[i] = new TestThread();
 }
 for (int i = 0; i < threads.length; i++) {
 threads[i].start();
 }
 }
}
class TestThread extends Thread {
 @Override
 public void run() {
 // 对于不同单例模式的实现,只需更改相应的单例类名及其公有静态工厂方法名即可
 int hash = Singleton5.getSingleton5().hashCode(); 
 System.out.println(hash);
 }
}

1、为什么说饿汉式单例天生就是线程安全的?

// 饿汉式单例
public class Singleton1 {
 // 指向自己实例的私有静态引用,主动创建
 private static Singleton1 singleton1 = new Singleton1();
 // 私有的构造方法
 private Singleton1(){}
 // 以自己实例为返回值的静态的公有方法,静态工厂方法
 public static Singleton1 getSingleton1(){
 return singleton1;
 }
}

我们已经在上面提到,类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

2、传统的懒汉式单例为什么是非线程安全的?

// 传统懒汉式单例
public class Singleton2 {
 // 指向自己实例的私有静态引用
 private static Singleton2 singleton2;
 // 私有的构造方法
 private Singleton2(){}
 // 以自己实例为返回值的静态的公有方法,静态工厂方法
 public static Singleton2 getSingleton2(){
 // 被动创建,在真正需要使用时才去创建
 if (singleton2 == null) {
 singleton2 = new Singleton2();
 }
 return singleton2;
 }
}

上面发生非线程安全的一个显著原因是,会有多个线程同时进入 if (singleton2 == null) {…} 语句块的情形发生。当这种这种情形发生后,该单例类就会创建出多个实例,违背单例模式的初衷。因此,传统的懒汉式单例是非线程安全的。

3、实现线程安全的懒汉式单例的几种正确姿势

同步延迟加载 — synchronized方法

// 线程安全的懒汉式单例
public class Singleton2 {
 private static Singleton2 singleton2;
 private Singleton2(){}
 // 使用 synchronized 修饰,临界资源的同步互斥访问
 public static synchronized Singleton2 getSingleton2(){
 if (singleton2 == null) {
 singleton2 = new Singleton2();
 }
 return singleton2;
 }
}

该实现与上面传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton2()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例

同步延迟加载 — synchronized块

// 线程安全的懒汉式单例
public class Singleton2 {

 private static Singleton2 singleton2;

 private Singleton2(){}
 public static Singleton2 getSingleton2(){
 synchronized(Singleton2.class){ // 使用 synchronized 块,临界资源的同步互斥访问
 if (singleton2 == null) { 
 singleton2 = new Singleton2();
 }
 }
 return singleton2;
 }
}

该实现与上面synchronized方法版本实现类似,此不赘述。从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率仍然比较低,事实上,和使用synchronized方法的版本相比,基本没有任何效率上的提高

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

// 线程安全的懒汉式单例
public class Singleton5 {
 // 私有内部类,按需加载,用时加载,也就是延迟加载
 private static class Holder {
 private static Singleton5 singleton5 = new Singleton5();
 }
 private Singleton5() {
 }
 public static Singleton5 getSingleton5() {
 return Holder.singleton5;
 }
}

如上述代码所示,我们可以使用内部类实现线程安全的懒汉式单例,这种方式也是一种效率比较高的做法,它与饿汉式单例的区别就是:这种方式不但是线程安全的,还是延迟加载的,真正做到了用时才初始化

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

阿里面试题

详细理解单例模式与多线程+阿里面试题+面试心得

  1. 多个线程同时读写,读线程的数量远远大于写线程,你认为应该如何解决并发的问题?你会选择加什么样的锁?
  2. JAVA的AQS是否了解,它是干嘛的?
  3. 除了synchronized关键字之外,你是怎么来保障线程安全的?
  4. Tomcat本身的参数你一般会怎么调整?
  5. 你有没有用过Spring的AOP? 是用来干嘛的? 大概会怎么使用?
  6. 如果一个接口有2个不同的实现, 那么怎么来Autowire一个指定的实现?
  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的区别。
  21. 实现一个保证迭代顺序的HashMap。
  22. 说一说排序算法,稳定性,复杂度。
  23. 说一说GC。
  24. JVM如何加载一个类的过程,双亲委派模型中有哪些方法?
  25. TCP如何保证可靠传输?三次握手过程?

面试心得

  1. 准备要充分,知识面要尽量的广,同时深度也要够。
  2. 面试安排上,如果不着急,尽量给自己留多时间,两天一家,及时做总结和补充。
  3. 心态要放平,当做一次技术交流,面试要看一部分的运气,也要看一些眼缘,有的面试官一张嘴你就能感觉到你这次面试完了。想去的公司没有面试好,不要气馁,继续加油准备。
  4. 简历投递方面,拉勾上投了很多经常不匹配,可能是我学历问题(自考本),有一些打击自信心,如果有同样感受的,不妨换BOSS或者其他平台。避免打击自信心。
  5. 写简历一定要体现自己的优势,最好能体现类似于,用到了什么技术,解决了什么问题。简历上写到的一定要胸有成竹。
  6. 类似于你的优势是什么,你觉得你项目中做的比较好的地方有哪些,你能给公司带来什么,这种问题心里要先想一些,免得临场发挥容易紧张说不好。

以上就是我整理的单例模式与多线程以及阿里的面试题和面试心得,写得不好,请大家多多批评指正,希望能对那些还在面试的你能够有所帮助!!

本文到此结束,喜欢的朋友点点赞和关注,感谢!!

猜你喜欢

转载自blog.csdn.net/qwe123147369/article/details/91523064