🙌🏻 最近有小老弟吐槽,面试回答单例模式不尽人意,被面试官pass了,趁此机会,我们来简单总结一波究竟什么是单例模式。
1、什么是单例模式
单例模式:顾名思义就是在整个运行时域,一个类只有一个实例对象,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)但是:在设计单例的时候要考虑很多问题,比如线程安全问题、序列化对单例的破坏等
2、为什么需要单例模式呢?
因为有的类的实例对象的创建和销毁对资源来说消耗不大(比如说String),有的类型呢,比较庞大和复杂,如果频繁的创建和销毁对象,并且这些对象是可以复用的,那么将会造成一些不必要的性能浪费。
🙋♂️个🌰子:比如说访问数据库,而创建数据库链接对象是一个耗资源的操作,并且数据库连接完全是可以复用的,那么我可以将这个对象设计成单例的,这样我只需要创建一次并且重复使用这个对象就行了,而不需要每次都去访问。
3、在java中如何实现单例模式呢?
3.1、常规懒加载代码如下(非线程安全)
public class Singleton {
private Singleton(){} // 构造器私有
private static Singleton instance = null;
public static Singleton getInstance() {
if( instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.2、懒加载-线程安全改造1
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if( instanice == null) {
instance = new Singleton();
}
return instance;
}
}
但是加入了sync有个问题,其实我们只想要对象在构建的时候同步线程,而这样的话,每次在获取对象时就都要进行同步操作,对性能影响非常大
3.3、[非]懒加载-线程安全改造2
线程安全问题出现在构建对象阶段,那么我们只要在编译期构建对象,在运行时调用,就不用考虑安全问题了。于是我们可以这么写:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance(){
return instance;
}
}
这么写能保证线程安全,但这不是懒加载的 - - 不推荐
3.4、懒加载-线程安全改造3(双检锁写法)
在安全改造1的基础上,形成低效的原因是 getInstance()方法中加入了synchronized, 所以每个进行该方法的线程首先都得获取到锁, 锁的粒度太大了。
public class Singleton {
private static Singleton singleton; // 定义对象
private Singleton ( ){}
public static Singleton getSingleton(){
if(singleton == null){// 第一步 常规的非空判断,没有对象才会去创建对象
synchronized (Singleton.class){
// 第二次判空,防止多个线程同时进来没拿到锁阻塞到第一步,进到这里的时候会创建多个对象
if (singleton == null) {
singleton = new Singleton(); // 第三步
}
}
}
return singleton;
}
}
3.5、懒加载-线程安全改造4
双检锁已经很安全了,但是!(这里就要谈到Java
多线程的happens-before
原则)
happens-before
原则有8
条:
- 1、程序顺序规则
- 2、锁定规则
- 3、volatile变量规则
- 4、线程启动规则
- 5、线程结束规则
- 6、中断规则
- 7、终结器规则
- 8、传递性规则
因为在上面第三步中,singleton = new Singleton();
不是原子操作,此行代码总共要执行三条指令:1、分配内存。2、初始化对象。3、对象指向内存地址。在真正执行时,JVM
虚拟机为了效率可能会对指令进行重排,比如说按照3、1、2的方式执行。那么怎么改进呢。此时用到另外一个关键字 volatile
-防止指令重排 ,防止a线程执行到new Singleton()
的时候此时对象还未创建,只是把内存地址申请下来了,然后b线程拿到锁又进来了,b线程以为这个对象已经创建完成,在第二个if (singleton == null)
处进行判断为false
,b
线程会直接return
, 这个时候就会有问题了,b
调用getInstance
方法会报错,因为出现了线程不安全的问题。(这也是指令重排会导致的有序性问题)
public class Singleton {
private volatile static Singleton singleton; // 定义对象
private Singleton ( ){}
public static Singleton getSingleton(){
if(singleton == null){// 第一步 常规的非空判断,没有对象才会去创建对象
synchronized (Singleton.class){
// 第二次判空,虽然只有一个线程能拿到锁,但是多个线程很有可能已经进入了if代码块,此时正在等待,假设两个线程a、b
// a先拿到锁,一旦线程a释放,线程b会立即获得锁,然后又进行对象创建,这样对象会被创建多次。
if (singleton == null) {
singleton = new Singleton(); // 第三步 在指令层面不是一个原子操作
}
}
}
return singleton;
}
}
3.6、既满足懒加载又满足线程安全-静态内部类
静态内部类在程序启动的时候不会加载,只有第一次调用的时候才会加载,这种写法巧妙的利用了JDK类加载的机制的特性来实现了懒加载。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.7、天然线程安全的类-枚举单例
public enum Singleton {
EXAMPLE_INSTANCE;
public void testMethod() {
}
}
为什么枚举是线程安全的?当我们反编译该class文件: javap -c Singleton.class
结果如下:
public final class Singleton extends java.lang.Enum {
public static final Singleton EXAMPLE_INSTANCE;
...(省略)
}
可以看到Singleton.java
是继承了Enum
类的,同时被final
关键字修饰:这个类是不能被继承的。并且EXAMPLE_INSTANCE
通过static
修饰,static
类型的属性会在类被加载之后才被初始化,当一个Java
类第一次被真正使用到的时候静态资源被初始化、Java
类的加载和初始化过程都是线程安全的。所以,创建一个enum
类型是线程安全的。