这篇博客主要参考b站寒食君视频:【单例模式】猛男因不懂单例模式,被面试官无情嘲讽_哔哩哔哩_bilibili
一、什么是单例模式,为什么需要单例模式?
单例模式:顾名思义就是在整个运行时域,一个类只有一个对象实例
为什么需要单例模式?
因为有的类的实例对象创建和销毁对资源消耗太大,有的类的实例对象创建和销毁对象消耗资源较小如String类,但是对于比较庞大复杂的类,需要频繁创建和销毁的对象话,并且这些对象是完全是可以复用的话,那么将造成不必要的资源浪费。
例如需要创建一个访问数据库的Demo而创建数据库连接对象是一个耗资源的操作,并且数据库连接完全是可以复用的那么就可以将数据库连接对象设计成单例的,这样就可以创建一次并且可以重复使用这个对象了,而不需要每次访问数据库都要创建一个新的数据库链接对象。
二、在Java中怎么实现单例呢?
单例模式一般有两种饿汉模式和懒汉模式,因为饿汉模式在初始化得时候就创建了,程序中有可能用不上,如何对象很大的话是需要浪费比较大的资源开销的,所有项目中一般懒汉模式用的多。我们本次内容也是按照懒汉模式展开讨论
实现单例模式主要考虑三点:
① 是否线程安全
② 是否懒加载
③ 是否反射破坏
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
1、将构造器设置为private,那么其他的类就无法通过new来构造这个Singleton对象实例了。因为构造一个类需要调用构造函数,构造函数私有之后如果在类的外部new对象无法调用构造函数导致报错。
2、因为无法通过new构造该类,那么只能通过调用类的getInstance得到该对象,可以看到实例对象在第一次使用的时候才真正被创建的而不是程序已启动就构建好了等你调用(后面的饿汉模式就是这样实现的)。这种滞后的构建的方式叫懒加载。
懒加载的好处:因为有的对象构建的开销比较大,假如这个对象在项目启动的时候就构建万一从来没有被调用过,那就比较浪费了,只有真正使用的时候才去构建这是比较合理的
3、再看看是否线程安全的,因为在执行if(instance==null)的时候,可能会有多个不同的线程同时进入,会导致实例化多次
改进1:添加synchronized,可以保证在同一时刻只有一个线程能够进入该方法
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance(){
if(instance==null){
instance = new Singleton();
}
return instance;
}
}
问题:其实我们只想对象在构建的时候同步线程,对于这个操作,每次获取对象时都要进行同步操作,这样对性能影响非常大,这种写法大多数情况都不可取
改进方向:线程安全问题出现在构建对象的阶段,那么我们只要在编译期构建在运行时调用就好了。
改进2:在编译期间就构建:
public class Singleton {
private Singleton(){}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
问题:但是这个又不是懒加载的,到底有没有一种写法既是线程安全的又是懒加载的呢?
改进3:回到第二种写法,懒加载,第二种写法形成低效的原因就是getInstance加了synchronized,导致每个进入该方法的线程都首先得获得锁,那么我们可以修改为在构建对象的时候同步,而如果可以直接使用对象的时候就没必要同步了,得到下面这种改进
public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){//1
if(instance==null){//2
synchronized(Singleton.class){//3
instance = new Singleton();//4
}
}
return instance;
}
}
分析:① 在getInstance不需要竞争锁,所有线程都可以进入,此时进入第2步判断,如果实例对象还没有创建,那么多个线程开始争抢锁,抢到手的那个线程开始创建实例对象,实例对象创建之后,以后所有的线程执行到第二步时,都可以直接跳过,直接返回instance对象,再也不用去争抢锁了,这就可以解决改进2中的低效问题。② 存在的问题:在多个线程执行了语句2后,虽然只有一个线程能够抢到锁去执行语句3创建实例对象,但是会有其他线程已经进入了if代码块,此时正在等待,一旦抢到锁的线程执行完毕等待的线程就会立即获得锁然后进行对象创建,那么对象就会被创建多次。
改进4:再增加一个if判断--双检锁写法
public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if(instance==null){//1
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();//2
}
}
}
return instance;
}
}
分析:如果有a,b两个线程同时进入第一个if条件,a线程先获取锁,当a线程释放锁的时候b立即获得锁,会进行一个判空操作,因为a线程执行完毕,实例对象已经创建成功,于是b线程就会直接退出返回实例,这样方式就不会造成线程不安全的问题了。这种对对象进行两次判空的操作叫做双检锁,双检锁在日常开发中是一种很常见的同步线程手段。那到这里就结束了么?老程序员看完代码眉头一皱,想到了happen-before原则:多线程开发中需要遵循happen-before原则(关注b站寒食君催更视频)
因为步骤2:instacne = new Singleton() 在指令层面不是一个原子操作,实际上它分为了三步:
① tmp=allocate(); //分配内存
② constructor(tmp); //真正执行构造函数初始化对象
③ instance=tmp;//对象指向内存地址(直到执行到这一步instance才不为null)这三步参考了:文章正在审核中... - 简书
在真正执行时,JVM虚拟机为了效率可能会对指令进行重排,指令真正执行的顺序可能是:①->③->②,看上面的代码,假设有一个线程a执行到new对象的第③步时,这时候instance不为null,但没有执行②,所以instance实际上没有被初始化,如果此时有一个b线程进来执到1位置判断instance的状态,因为a线程执行了①和③,那instance已经不是null,会导致b线程直接返回一个没有初始化的instance,这就导致灾难的发生。
改进5:通过一个volatile解决上述问题
public class Singleton {
private Singleton(){}
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance==null){//1
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();//2
}
}
}
return instance;
}
}
由于 volatile禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。上述执行步骤只能是①->②->③。
更多volatile相关参考这里:死磕Java——volatile的理解 - 简书
后面读一下这篇文章更多关于volatile和synchronized:既然synchronized是"万能"的,为什么还需要volatile呢? - HollisChuang - 博客园
改进6:满足懒加载+线程安全+高效的方法
这里用到了静态内部类(关注b站寒食君催更视频)在这里只需要理解【静态内部类在程序启动的时候不会加载,只有第一次调用的时候才会加载】这种写法巧妙的利用了JDK类加载机制的特性来实现懒加载:
public class Singleton {
private static class SingletonHander{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHander.INSTANCE;
}
}
反射破坏?上诉所以的方式都可以使用反射来破坏,但是反射是一种人为的主动操作,只有故意这样才会导致反射破坏(关注b站寒食君催更视频)在这里可以理解反射就是在程序运行时,动态地调用一些类型信息。