> 文档中心 > 设计模式------单例模式

设计模式------单例模式

一,简介

        单例模式是23种设计模式里面最简单的一个设计模式,该模式保证我们的bean对象,从始至终只有一个对象,它提供了创建对象的一种最佳方式,哪里需要用到此单例对象直接拿过来使用就可以,由于他自始至终只有一个对象,因此他节省了我们的内存空间,可以避免咱们的程序创建大量的对象,进而产生内存溢出的情况.

        单例模式确保某个类只有一个实例而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态。

二,特点

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例.
  4. 单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性

 三,单例的四大原则

  1. 构造私有
  2. 以静态方法或者枚举返回实例
  3. 确保实例只有一个,尤其是多线程环境
  4. 确保反序列换时不会重新构建对象

四,实现方式 

实现单例模式有多种方式,根据他们的特点又可以分为延迟加载和立即加载,立即加载我们又叫做饿汉式, 延迟加载我们有叫做懒汉式,这种方式只有我们使用到的时候才会加载他.下面就来跟随小编的步伐一一实现他吧.

1.饿汉式

        饿汉式顾名思义,我立刻就要到的我的对象来使用它,饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。

代码实现

/** * @program: Spring-Study * @description:饿汉式单例模式 * @author: Mr ZHAN * @create: 2022-05-24 16:27 **/public class HungrySingleton {    //②创建完整对象    private static HungrySingleton hungry=new HungrySingleton();    //①构造器私有    private HungrySingleton() {    }    //③向外界暴露    public static HungrySingleton getInstance() { return hungry;    }}

测试:

为了证明我们的bean对象是线程安全的,我们采用普通测试和启用多线程进行测试

/** * @program: Spring-Study * @description:饿汉式单例模式 * @author: Mr ZHAN * @create: 2022-05-24 16:27 **/public class HungrySingletonTest {    public static void main(String[] args) { //单个测试 HungrySingleton instanceA = HungrySingleton.getInstance(); HungrySingleton instanceB = HungrySingleton.getInstance(); System.out.println("instanceA = " + instanceA);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 System.out.println("instanceB = " + instanceB);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 //启用多线程测试 for (int i = 0; i {  HungrySingleton instance = HungrySingleton.getInstance();  System.out.println("instance = " + instance);     }).start(); }    }}

经过测试发现无论我们采用那种方式进行测试,他的地址值都是同一个.

2.饱汉式

饱汉式指的是只有我们在使用对象的时候才会去加载,如上面所示,我就坐着不动,我啥时候饿了,啥时候去吃饭,他与饿汉式不一样的是,饿汉式是主动出击,立马加载

代码实现

/** * @program: Spring-Study * @description:饱汉式-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton {    //②创建完整对象    private static FullSingleton full;    //①构造器私有    private FullSingleton() {    }    //③向外界暴露    public static FullSingleton getInstance() { if (full==null){     full=new FullSingleton(); } return full;    }}

 测试:同样采用两种方式测试

/** * @program: Spring-Study * @description:饱汉式-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingletonTest {    public static void main(String[] args) { //普通测试 HungrySingleton instanceA = HungrySingleton.getInstance(); HungrySingleton instanceB = HungrySingleton.getInstance(); System.out.println("instanceA = " + instanceA);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 System.out.println("instanceB = " + instanceB);//com.dahai.singlton.hungry.HungrySingleton@71bc1ae4 //启用多线程测试 for (int i = 0; i {  FullSingleton instance = FullSingleton.getInstance();  System.out.println("instance = " + instance);     }).start(); }    }}

多线程测试结果:

          根据实际的测试情况来看,不难发现,在非多线程的情况下,所创建的对象能够保持单例,但是在多线程的情况下出现了地址值不一样的情况,也就意味着产生了多个不同的对象,饱汉式在单线程的情况下能够正常保持对象是单例的,但是在多线程就未必了,因此我们采用同步锁来进行优化

3.饱汉式优化一

        在方法上加synchronized同步锁或是用同步代码块对类加同步锁

代码实现

/** * @program: Spring-Study * @description:饱汉式优化一-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton {    //②创建完整对象    private static FullSingleton full;    //①构造器私有    private FullSingleton() {    }    //③向外界暴露    public static synchronized FullSingleton getInstance() { if (full==null){     full=new FullSingleton(); } return full;    }}

 扩展:synchronized关键字是一把本地同步锁,可以自动的解锁,加锁,方法的同步是隐式的,代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。通过javap命令可以看到,他可以将关键字放在实例方法上,静态方法上以及同步代码块来保证线程的同步.但他们的同步锁却不相同

①实例方法:锁对象为this关键字(当前对象)  

②静态方法:锁对象为类.class

③同步代码块:锁对象为类.class

此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行,因此我们可以采用双重检查锁来进行优化,使获取实例对象的方法仅对需要的请求有效.

4.饱汉式优化二(提高同步锁的效率)

使用双重检查锁,可以避免整个方法被锁,使得我们的方法只对需要的实例提供对象,提高同步锁的效率

代码实现

package com.dahai.singlton.full;import com.dahai.singlton.hungry.HungrySingleton;/** * @program: Spring-Study * @description:饱汉式优化二-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class FullSingleton {    //②创建完整对象    private static volatile FullSingleton full;    //①构造器私有    private FullSingleton() {    }    //③向外界暴露    public static  FullSingleton getInstance() { if (full==null){     synchronized (FullSingleton.class) {  if (full==null){      full=new FullSingleton();  }     } } return full;    }}

        同样的方法去测试,可以保证咱们的对象都为单例,细心的朋友可能会发现我在此代码中使用了volatile关键字由于咱们的jmm内存模型分为核心内存和工作内存两部分,每一个线程会在自己的工作内存里面完成,于是有了这个关键字,他可以保证可见性,不保证原子性,禁止指令重排,此处使用他的目的是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题

 5.静态内部类实现

        这种方式引入了一个内部静态类(static class),静态内部类只有在调用时才会加载,它保证了Singleton 实例的延迟初始化,又保证了实例的唯一性。它把singleton 的实例化操作放到一个静态内部类中,在第一次调用getInstance() 方法时,JVM才会去加载InnerObject类,同时初始化singleton 实例,所以能让getInstance() 方法线程安全。

特点是:即能延迟加载,也能保证线程安全。

/** * @program: Spring-Study * @description:静态内部类-单例模式 * @author: Mr ZHAN * @create: 2022-05-24 17:45 **/public class StaticSingleton {    //①构造器私有    private StaticSingleton() {    }    //②静态内部类延迟加载   public static class InnerObject{ private  static StaticSingleton staticSingleton=new StaticSingleton();   }   //向外暴露    public static StaticSingleton getInstance(){ return  InnerObject.staticSingleton;    }}

        同样采用多线程测试,可以看到获得的线程都是单例的,静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

 6.枚举类实现(防止反射和反序列化攻击)

事实上,通过Java反射机制是能够实例化构造方法为private的类的。这也就是我们现在需要引入的枚举单例模式。

6.public class SingletonFactory {  7.  8.    /** 9.     * 内部枚举类 10.     */  11.    private enum EnumSingleton{  12.        Singleton;  13.        private Singleton6 singleton;  14.  15.        //枚举类的构造方法在类加载是被实例化  16.        private EnumSingleton(){  17.            singleton = new Singleton6();  18.        }  19.        public Singleton6 getInstance(){  20.            return singleton;  21.        }  22.    }  23.      24.    public static Singleton6 getInstance() {  25.        return EnumSingleton.Singleton.getInstance();  26.    }  27.}  28.  29.class Singleton6 {  30.    public Singleton6(){}  31.}  

五,常用项目使用 

通常情况下我们使用线程池,需要使线程池对象为单例的,一般情况下我们在spring下开发,由于spring的对象默认为单列,因此我们可以直接将我们的线程池对象,交由我们的spring的ioc容器去管理,详情见以下代码

/** * @program: Spring-Study * @description: * @author: Mr ZHAN * @create: 2022-05-24 18:42 **///声明他是一个配置类@Configurationpublic class ThreadPoolApplication {    /**     * 创建线程池对象交由ioc     * 七个核心参数     * ①核心线程数     * ②最大线程数     * ③存活时间     * ④单位     * ⑤等待队列     * ⑥线程工厂     * ⑦拒绝策略(4种 1.直接拒绝 2.交由调用者处理  3.丢弃等待时间最长  4.报错)     *     */    @Bean    public ThreadPoolExecutor  getThreadPoolExecutor(){ return new ThreadPoolExecutor(3,  6,  100,  TimeUnit.SECONDS,  new ArrayBlockingQueue(3),  Executors.defaultThreadFactory(),  new ThreadPoolExecutor.AbortPolicy());    }}

如何选择线程数:

线程数的选择根据cpu的核数n来判断,如果程序为cpu密集型,cpu会一直工作,利用率非常高,通常设置为n+1,如果程序为io密集型,整个程序会发生io操作,整个系统cpu的利用率不是特别足,因此通常最大线程数设置为2n

六,总结

单例模式是最简单的一种设计模式,无外乎就是实例bean只能存在一个,通常有一个思路

①构造器私有

②本类创建实例

③将单实例暴漏给外界供外界使用

此文是作者纯手工打造,方便大家学习,转载请注明出处!