设计模式——单例模式

  • Post author:
  • Post category:其他


一,简介

单例模式是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 < 30; i++) {
            new Thread(()->{
                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 < 400; i++) {
            new Thread(()->{
                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
 **/
//声明他是一个配置类
@Configuration
public 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只能存在一个,通常有一个思路

①构造器私有

②本类创建实例

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


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



版权声明:本文为weixin_45766875原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。