设计模式-单例模式
单例模式定义:
单例模式的英文叫做singleton模式,单例模式就是,在你的系统里,你要判断一下,如果有一些类,只需要一个实例就可以了,那就给那个类 做成单例的模式。
单例模式常见的场景:
实际上,如果结合 业务需求 在实际项目里面,实践过的,或 使用过的设计模式,单例模式和工厂模式是最常用的两种。那么,单例模式常见的场景,可能就两类。
第一类 就是,比如说 我们自定义了一个框架,然后针对这个框架,自定义了一份xml格式的一个配置文件,要读取这个配置文件,把这个配置文件中的数据 读取到类中。
如果 这个类的实例,只要保存一份就可以;那么此时,就可以使用单例模式,将这个类做成,它的这个实例 只能有一个;然后,在这个实例中,保存了配置文件中的数据。ok,这是第一个场景。
第二个场景 就是,类似于,我们在使用工厂模式时,在工厂模式里面,其实我们有些这个工厂 是需要实例化对象的,因为要基于 这个实例化对象,然后来实现一些,比如 继承,接口 或 其他实现 等功能,那么,像这些工厂的实例,就可以做成这个单例的。
那 除此之外的 其他情况下,需要自己去判断,如果是一个类的这个实例,只需要保存一份,那就做成单例。
综上,单例具体什么时候用,在做这个系统的时候,一般来说 都能判断出来。但其实 比较重要的是,这个单例怎么来实现。这个单例所谓的实现,大体上 可以分成两类,一种叫做 饱汉模式,一种叫做 饿汉模式。
单例模式的实现1-饿汉模式:
那 什么叫做 饿汉模式,就很简单,直接showCode:
/** * 饿汉模式 */ public class HungrySingletonPatternDemo { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); singleton.execute(); // 如果把 Singleton 这个类放到外面去,而不是以内部类的形式出现,这时去new Singleton()的这个构造函数,就会报错 //new Singleton(); } public static class Singleton { /** * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量 * * static:就是一个类的静态变量 * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了 */ private static final Singleton instance = new Singleton(); /** * 第二步:将构造函数搞成private私有的 * * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象 */ private Singleton(){} /** * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例 * @return */ public static Singleton getInstance() { return instance; } public void execute() { System.out.println("单例类的方法"); } } }
先写一个 所谓的这个饿汉模式,创建一个类,叫做HungrySingletonPatternDemo,就是这个饿汉模式。然后,定义一个内部类Singleton,public static class Singleton {},在这个Singleton的里面,第一步,我们在这先把它写出来,就是private static final Singleton instance = new Singleton(),直接就是,将这个类的实例在这里创建出来,然后赋予这个static final修饰的变量,这个static的意义是说,就是一个类的静态变量,这个final是说,这个变量的引用 第一次初始化赋予之后,就再也不能修改这个引用了。这些都是java的基础。也就是,在Singleton它这个类初始化的时候,第一步的 它这一行代码就会执行,然后 类一初始化,直接就把 它自己的实例 给创建出来了。
然后, 第二步其实就是,这个private Singleton() {},将它的构造函数搞成这个private 私有的,此时 除了这个类自己本身,其他任何人 都不能创建它的 这个实例对象。ok,这个是第二步。
然后,第3步的话,就是 public static Singleton getInstance() {return instance;},给它一个static 静态方法,然后,返回自己唯一的 内部创建的 一个实例。
最后,我们还可以 给它一个方法,public void execute() { System.out.println("单例类的方法");}。
然后,我们来用一下这个类,写一个main方法,Singleton singleton = Singleton.getInstance();,然后,singleton.execute();,就这样吧。
执行一下main方法,来看一下,ok,单例类的方法。
其实为什么叫它是饿汉模式,因为 这个类,可以认为 它很饿,好着急。饿,指的就是说,它好像就是一种很饥渴的状态。所以说,它因为很饿,过于饥渴,就着急忙慌的,在自己这个类初始化的时候,private static final Singleton instance = new Singleton(),这一行代码就会执行,直接就是把自己的这个对象实例,给创建出来,给它初始化出来了,所以给它叫做饿汉模式。
但是,你会发现,在单例这种模式下,这个Singleton类的实例,全局就只有一个,就只有它,因为 外面不能 再对它去创建 它的实例了,比如说,你要是 在main方法中,再new一个Singleton,这样new Singleton(),那肯定会报错。但因为当前,我们这个Singleton是HungrySingletonPatternDemo的这个内部类,所以不会报错。但是如果把Singleton这个类,给放到外面去。
public class Singleton { /** * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量 * * static:就是一个类的静态变量 * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了 */ private static final Singleton instance = new Singleton(); /** * 第二步:将构造函数搞成private私有的 * * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象 */ private Singleton(){} /** * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例 * @return */ public static Singleton getInstance() { return instance; } public void execute() { System.out.println("单例类的方法"); } }
我再写一个Singleton类 去演示出来,然后HungrySinglePatternDemo内部的Singleton这个类,这边把它给注掉,复制到外面这个Singleton中,这样把Singleton就放到外面去了。
/** * 饿汉模式 */ public class HungrySingletonPatternDemo { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); singleton.execute(); // 如果把 Singleton 这个类放到外面去,而不是以内部类的形式出现,这时去new Singleton()的这个构造函数,就会报错 new Singleton(); } // public static class Singleton { // // /** // * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量 // * // * static:就是一个类的静态变量 // * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了 // */ // private static final Singleton instance = new Singleton(); // // /** // * 第二步:将构造函数搞成private私有的 // * // * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象 // */ // private Singleton(){} // // /** // * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例 // * @return // */ // public static Singleton getInstance() { // return instance; // } // // public void execute() { // System.out.println("单例类的方法"); // } // } }
这时,在HungrySingletonPatternDemo类中的main这里,如果去new Singleton()的这个构造函数,去创建它的实例,是不行的,它会提示说,The constructor Singleton() is not visible,就是说它的Singleton的 这个构造函数是私有的。饿汉单例模式,会在类初始化的时候,就被创建出来,而且全局只有一个,所以,一定是线程安全的。那么,这个就是所谓的 饿汉模式。
单例模式实现2-线程不安全的饱汉模式:
然后的话,是线程不安全的饱汉模式,那个叫做,UnsafeFullSingletonPatternDemo。代码如下:
/** * 线程不安全的饱汉模式 */ public class UnsafeFullSingletonPatternDemo { /** * 线程不安全 */ public static class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { /** * 假设有两个线程过来 * * 线程的基础:线程是并发着执行的,cpu,先执行一会儿线程1,然后停止执行线程1;切换过去执行线程2 * 执行线程2一会儿之后,再停止执行线程2;回来继续执行线程1 * * 第一个线程,判断发现说instance == null,代码进入到了下面去 * 第二个线程,执行到这儿,发现,此时instance == null,那么就没什么问题了,继续往下走 */ if(instance == null) { // 第一个线程跑到了这儿来,但是此时第一个线程,还没有执行下面的那行代码 // 此时,第二个线程代码也执行到了这儿,cpu切换回线程1 // 执行线程1的代码,线程1会创建一个实例出来 // 但是切换到线程2去执行的时候,线程2 的代码已经执行到这儿来了,此时又会再一次执行下面的代码 // 就是会再一次创建一个实例,之前线程1创建的那个实例,就会被垃圾回收,废弃掉了 instance = new Singleton(); } return instance; } } }
首先什么叫饱汉模式,饱汉模式,它的意思就是说,我这肚子好撑啊,实在是不想在上来,一开始的时候,就创建我的这个实例,因为我太撑了,我是这个饱汉,我并不饥渴,所以说我不想,在这个类初始化的时候,上来就在这,有个private static Singleton instance,声明这个单例持有自己本身变量的时候,就创建我的这个对象实例。
然后,我希望是,通过一个对外的静态方法,public static Singleton getInstance(){},当然还是要给个private Singleton(){},一个私有的无参构造器。我希望是说,如果有人,要来获取的我的实例,我在对外的getInstance方法中,这边来给singleton的Instance,给它判断一下,if(instance == null){instance = new Singleton} return instance,如果它是null的话,new Singleton,然后,给它返回instance,就这样子。
也就是说,Singleton这是一个单例类,它说,我肚子好撑,我是饱汉;然后,我不想上来就给它,初始化这个实例instance;然后我提供一个方法,这个方法里面说,你来调用的时候,如果你第一次来调用,如果我发现说,这个实例instance是null,我就给它创建好实例,然后给它返回,那么后面的话呢,再有请求过来,直接去用,我之前创建好的这个实例就可以了。然后,这个 main class就不用写了,没有什么好演示的,因为太简单了。
但是这个,为什么说是 线程不安全的,它之所以称之为线程不安全,主要的问题出在这,getInstance方法,假设有两个线程过来。然后,第一个线程,判断发现说,这个instance == null,然后这个代码就进入到了if里面去,那个第一个线程跑到了这来,instance = new Singleton();,ok,稍等,但是此时第一个线程,还没有执行下面的,instance = new Singleton(),这行代码。那首先这里需要有一个线程的基础,就是有一个前提,这个线程,它是并发着去执行的。就是在你的系统里,你可以认为系统的这个cpu,它是先执行一会线程1,然后停止执行线程1,然后,切换过去执行线程2,然后执行线程2,一会之后,再停止执行线程2,然后,回来继续执行线程1,所以,它是这样一个概念。
那么,第一个线程它先跑过来,if(instance == null) {instance = new Singleton},它说,这是null啊,然后第一个线程的代码执行到了instance = new Singleton这里,cpu给它停了,切换到了第二个线程,然后,第二个线程执行到这,if(instance == null) {instance = new Singleton},那发现此时instance,还是null啊,因为第一个线程,还没有去执行instance = new Singleton这行代码呢,所以第二个线程,就说这个instance还是null,那么就没什么问题了,然后,继续往下走,此时第二个线程的代码也执行到了instance = new Singleton这。那这个时候,cpu切换回线程1, cpu去执行线程1的那个代码,然后线程1就会来执行这个代码instance = new Singleton,线程1会创建一个实例出来,然后线程1就走完了,调用者它可以拿到这个实例。
但是,切换到这个线程2去执行,这个时候,这个线程2 的这个代码,已经执行到instance = new Singleton这来了,就是线程2的代码,已经在if判断成立的这个括号里面了,那么此时,线程2又会执行下面的那个代码instance = new Singleton,就是会再次创建一个实例,然后之前线程1创建的那个实例,就会被垃圾回收,因为就没有人引用了,就已经被废弃掉了。所以,就是因为会有这样的一个,线程不安全的这种情况,可能会去导致,instance这一个实例创建多次,所以它叫做线程不安全的。
线程不安全,就是多线程并发 去访问一个共享资源,private static Singleton instance,这就是一个共享资源的时候,因为线程来回来去的切换,可能就会导致,if(instance == null) {instance = new Singleton},这里就会有线程不安全的问题。这个就叫做线程不安全的饱汉模式。
单例模式实现3.1-不完全线程安全的饱汉模式:
那么,如果要做成线程安全的话,有一个常规性的一个方法,应该怎么来做呢,直接先show code:
public class SafeFullSingletonPatternDemo { public static class Singleton { private static Singleton instance; private Singleton() { } // 不是完美的 // 因为不同的JVM的编译器的问题,可能导致说,这个情况下,还是线程不安全的 // 具体的我不在这儿讲,因为涉及到复杂的JVM内部的原理:指令重排 public static Singleton getInstance() { // 如果线程1和线程2都执行到了这一步,然后此时线程1判断发现还是null // 线程2此时判断发现instance == null,也会进去 if (instance == null) { // 线程1就会进来,此时线程1停止,切换到线程2 // 线程2也会进来,此时切换到线程1 // 线程1,发现这里需要加锁,在这里加锁,获取到了这个锁 // 线程2过来,线程2发现说,我也想要在这里加锁,发现说这个锁被人加了,线程2挂起等待别人释放锁 // 此时切换回线程2,线程2发现锁被释放,然后在这里加锁 synchronized (SafeFullSingletonPatternDemo.class) { // 线程1就进来了,此时切换到线程2 // 切换回线程1,线程1此时在这里,再次判断,instance == null // 线程2就进来了,double check,如果这里没有instance == null的判断,那么线程2就会再次创建一个实例 // 但是这里是双重检查,线程2又判断了一下,instance == null?否,不是null if (instance == null) { // 线程1就会进来,创建一个实例 instance = new Singleton(); } } } // 这边出来以后,线程1就释放锁了 // 线程2跳出来,直接获取一个instance返回了,这个instance就是之前线程1创建的实例 return instance; } } }
创建一个SafeFullSingletonPatternDemo类,再添加一个内部类,public static class Singleton {},然后它说,我是这个饱汉吧,不需要一上来,初始化这个类的时候,就进行实例化,所以只需要声明一下instance即刻,private static Singleton instance; ,再加一个私有的无参构造器 private Singleton(){},和一个getInstance方法,public static Singleton getInstance(){},这个getInstance方法里面,它先判断一下,if(instance == null),如果它是null的话,ok,synchronized,加锁。因为它是一个静态方法,所以它加的这个锁,只能加在它的.class上面,也就是synchronized(SafeFullSingletonPatternDemo.class) {},然后这个锁里面,还得再来判断一下,如果instance是null的话,然后就执行instance = new Singleton();,就是if(instance == null) { instance = new Singleton},最后,将return instance;,将创建好的实例instance返回。这样的一个逻辑。
public static Singleton getInstance() { if (instance == null) { synchronized (SafeFullSingletonPatternDemo.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }
以上这个东西,叫做double check,双重校验锁。这个是什么意思,可以再来想一下,如果这时,那个线程1,和线程2,都执行到了这一步,第一个if(instance == null){},然后,此时线程1,判断发现,instance还是这个null,对吧,那么线程1,就会进来,准备执行synchronized(SafeFullSingletonPatternDemo.class){},此时线程1,停止,切换到线程2。
然后那个,切换到了线程2了以后,线程2,这个时候还在第一个if(instance == null){} 外面,线程2此时,判断发现,这个instance也是等于null啊,那么也会进去,对吧,然后此时线程2也会进来,准备执行synchronized(SafeFullSingletonPatternDemo.class){},此时切换到线程1。
然后那个,线程1发现了,这里需要加锁,那线程1,它就在这里加锁,然后线程1,获取到了这个锁,它加锁成功了,对吧。那么线程1呢,就进来了,准备执行第二个if(instance == null){},对吧,然后此时,切换到线程2。
然后那个,线程2,这时候在这,准备执行synchronized(SafeFullSingletonPatternDemo.class){},对吧,然后线程2过来,发现说,哎,我也想要在这里加锁啊,结果尝试加锁,发现说,这个锁被人加了啊,所以说,线程2挂起,等待别人释放锁,对吧,线程2就卡这了。
然后那个,此时再切换回 这个线程1,线程1此时在这里,执行第二个if(instance == null){},再次判断这个instance,它还是这个null,然后这个线程1,就会进来,进入第二个if(instance == null){}里面,执行instance = new Singleton();,创建一个实例,对吧。然后这个线程1,创建完了以后,就啪嚓一下将锁释放,准备return instance;,return之后,线程1就执行完了。
然后那个,线程1这边,创建instance实例,出来以后,线程1 就释放锁了,线程1释放锁了以后呢,如果此时,切换回这个线程2,线程2执行synchronized(SafeFullSingletonPatternDemo.class){},发现锁被释放,然后这个线程2,在这里加锁,线程2就进来了,执行第二个if(instance == null){}判断,instance这个时候,因为之前已经被线程1给创建出来了,所以它已经不是null了,所以不会再次创建一个Singleton实例。
所以说这边,为什么叫做doublecheck,就是说为什么锁前也要加一个if(instance==null),下面锁的里面,为什么又要加一个if(instance==null),因为如果第一个if(instance==null)这里,没有instance是null的这个判断,那么线程2,就可能会,在线程1释放锁之后,返回已经创建好的实例之前,再次进入锁里,创建一个实例,但是这里是双重检查,然后这个线程2,又判断了一下,这个instance是否为null呢,发现是否,instance这个时候,因为之前已经被线程1给创建出来了,所以它已经不是null了,否。然后线程2就直接,if就没有执行成立,啪,跳出来了,线程2跳出来了,然后,直接获取一个instance返回了,这个instance,就是之前线程1创建的这个实例,ok。
单例模式实现3.2-完全线程安全的饱汉模式:
好, 那么,所以说这个东西就是说,用doublecheck去确保线程的这个安全,但是的话呢,要说一点,就是像这个doublecheck的单例模式,其实它还是,不是完美的。因为这个不同的JVM的编译器的问题,可能导致说,在doublecheck单例模式的这个情况下,还是线程不安全的。因为涉及到复杂的jvm内部的这个原理,具体的参见 JMM内存模型之指令重排原则等。所以这种doublecheck双重检查的单例模式,它也不是说完全线程安全的。
那如果要做到彻底的线程安全,要怎么做呢,要做成,那个叫做 InnerClassFullSingletonPatternDemo,就是基于内部类来实现这个Singleton。先声明这个Singleton的单例类,public static class Singleton {},然后内部给它一个私有的无参构造器 private Singleton {},然后这边是在Singleton类的内部,再添加一个内部类,public static class InnerHolder {},然后这个InnerHolder的内部是,声明一个静态的不可变的常量instance,并初始化赋值new Singleton(),public static final Singleton instance = new Singleton();,最后,在Singleton中,提供一个静态方法,返回这个InnerHolder.instance,public static Singleton getInstance(){ return InnerHolder.instance;},ok。showCode如下:
/** * 这个才是我们实际开发过程中,最最常用的单例模式,内部类的方式来实现 */ public class InnerClassFullSingletonPatternDemo { /** * 可以做到饱汉模式 * * 内部类,只要没有被使用,就不会初始化,Singleton的实例就不会创建 * * 在第一次有人调用getInstance方法的时候,内部类会初始化,创建一个Singleton的实例 * * 然后java能确保的一点是,类静态初始化的过程一定只会执行一次 * */ public static class Singleton { private Singleton() { } public static class InnerHolder { public static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return InnerHolder.instance; } } }
这个是 完全线程安全的,而且它这个,可以做到饱汉模式。就是它的实例,上来是没有初始化的,然后,是被放在了它的一个 内部类InnerHolder里面,这个内部类,只要没有被使用,就不会初始化,那么那个Singleton的这个实例,就不会创建。就
public static final Singleton instance = new Singleton();
这一行就不会执行。那么在第一次有人调用那个,getInstance()这个方法的时候,第一次有人调用它,那么我们就返回那个内部类里面的这个instance实例。也就是这个时候,你第一次使用它getInstance方法,那么这个内部类会初始化,然后创建一个Singleton的这个实例。
也就是,第一次有人调getInstance()方法的时候,然后这个java能确保的一点是,就是类静态初始化的这个时候,类静态初始化的这个过程,一定只会执行一次,也就是说这个内部类,它一定只会初始化一次。
简言之,在第一次有人调用getInstance()这个方法的时候,第一次执行到这的时候,然后它会初始化,那只有 它唯一的一次初始化的时候,然后才会创建Singleton这个实例,基于内部类来实现的Singleton,这个东西能够保证,首先,它的这个实例,不是上来就创建的,它可以保证一个饱汉的模式,然后当你第一次需要的时候,第一次调用getInstance()这个方法的时候,InnerHolder.instance被执行,内部类InnerHoder中的instance它初始化,然后才会创建出来它。而且它一定是线程安全的,因为这个类的静态初始化的这个过程,这个jdk,就是说这个jvm给你保证,它一定只给你保证执行一次,一定不会说因为有多线程,这段代码执行多次,一定不会,不可能的一件事情。
好,所以说这个 基于内部类来实现的Singleton,才是我们实际开发过程中,最最常用的这个单例模式,就是用这个内部类的方式来实现,这个是最最常用的,而且是最最保险的,最最安全的,最最稳妥的。那么以上4个单例类,基本上到这就差不多了。
那么,在项目里的话,单例模式其实还是有很多地方可以去实践的,比如说一些,对一些工厂的这个实例,结合一些工厂模式来做。就是说对一些工厂的实例 可以做成是单例的,这个都没有问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!