重学设计模式(三设计模式-单例模式)

Posted 穆瑾轩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学设计模式(三设计模式-单例模式)相关的知识,希望对你有一定的参考价值。

1、设计模式简介

   ​ GoF中一共收录了23个设计模式,每个设计模式都旨在解决不同场景的问题。

1.1、设计模式分类

   ​ 所有模式都可以按其意图或目的进行分类。

1.1.1、按意图划分

意图模式特点
接口型模式适配器模式、外观模式、组合模式、桥接模式需要对一个类或一组类的方法进行定义或重定义的场景
职责型模式单例模式、观察者模式、中介者模式、代理模式、职责链模式、享元模式用于集中、加强以及限制普通对象责任的技术
构造型模式建造者模式、工厂方法模式、抽象工厂模式、原型链模式、备忘录模式让客户类不通过构造函数来创建对象
操作型模式模板方法模式、状态模式、策略模式、命令模式、解释器模式将操作分散,不同的类在实现同一个操作时采用不同的方式(即多态的灵活运用)
扩展型模式装饰器模式、迭代器模式、访问者模式主要用于扩展,装饰器(动态组合)、迭代器(顺序访问)、访问者(运行新定义操作)

1.1.2、按目的划分

   ​ 设计模式按目的来划分可以分为三大类,分为创建型模式、结构型模式和行为型模式 3 种。

   ​ 如果还需要分的更细,根据模式是主要用于类上还是主要用于对象上来分,又可分为类模式和对象模式两种。

1)按目的划分

意图模式特点
创建型模式单例模式、原型模式、简单工厂模式、工厂方法模式、抽象工厂模式、建造者模式关注如何创建对象,将对象的创建与使用分离
结构型模式代理模式、适配器模式、桥接模式、装饰器模式、外观模式、享元模式、组合模式关注如何将对象和类按某种布局组装成更大的结构
行为型模式策略模式、命令模式、责任链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式关注类或对象之间的如何协同完成复杂的任务

2)按作用范围划分

   ​ 类模式:工厂方法模式、模板方法模式、解释器模式、(类)适配器模式,适配器支持两种,还有一种是对象适配器模式。

   ​ 对象模式:除了类模式,剩下的都是对象模式。

2、单例模式

2.1、什么是单例模式

  • 定义

   ​ 单例(Singleton)模式:是一种创建型设计模式,一个类只有一个实例,同时提供对该实例的全局访问点。

2.2、模式的优缺点

  • 提问:单例模式违反了单一职责原则,那为什么单例模式还有存在的必要?

   ​ 我们知道每个模式的存在,都是为了解决某种问题,同样单例模式也有它的优点。

  • 优点

   ​ 1)确保一个类只有一个实例,单例对象仅在第一次被请求时才被初始化;

   ​ 为什么有人想要控制一个类只有一个实例?最常见的原因就是控制对某些共享资源的访问,达到节约资源的目的,频繁的去创建与销毁,会消耗我们一部分系统资源来处理这个过程。比如:I/O(配置信息类)与数据库连接等;

   ​ 2)为该实例提供全局访问点。

   ​ 就像全局变量一样,单例模式允许您从程序中的任何位置访问某个对象。

  • 缺点

   ​ 1)违反了单一职责原则;功能都写在一个类中,基于一个对象。

   ​ 2)在多线程环境中需要进行特殊处理,以便多个线程不会多次创建单例对象;

   ​ 3)在并发测试中,单例模式不利于代码调试,也不能模拟生成一个新的对象。

2.3、创建的方式

   ​ 许多开发人员认为单例模式是一种反模式。这就是为什么它在 Java 代码中的使用量正在下降的原因。尽管如此,Java 核心库中还是有相当多的 Singleton。比如:

java.lang.Runtime.getRuntime();  //Runtime的getRuntime()方法
java.awt.Desktop.getDesktop();  //Desktop的getDesktop()方法
java.lang.System.getSecurityManager();  //getSecurityManager()方法

   ​ java单例创建的方式有8种:

序号方式
1饿汉式(静态常量,线程安全)
2饿汉式(静态代码块,线程安全)
3懒汉式(线程不安全)
4懒汉式(同步方法,线程安全)
5懒汉式(同步代码块,线程不安全)
6双重检查(线程安全)
7静态内部类(线程安全)
8枚举(线程安全)

2.3.1、饿汉式(静态常量,线程安全)

   ​ 类的实例是在类加载时创建的,这是创建单例类的最简单方法。

public class SingletonEagerTest 
 public static void main(String[] args) 
  SingletonEager instance1 = SingletonEager.getInstance();
  SingletonEager instance2 = SingletonEager.getInstance();
  
  System.out.println(instance1==instance2); //true
  System.out.println(instance1.hashCode()); //hashCode值相同
  System.out.println(instance2.hashCode());
 


/**
 * 饿汉式(静态常量,线程安全)
 */
class SingletonEager
 private static final SingletonEager instance = new SingletonEager();
 //私有构造函数避免客户端应用程序使用构造函数
 private SingletonEager()
 
 public static SingletonEager getInstance()
        return instance;
    

  • 优点

   ​ 写法简单,且是线程安全的,因为在类装载的时候就完成了实例化。如果单例类不使用大量资源可以使用这种方法。

  • 缺点

   ​ 即使客户端应用程序可能不使用它也会创建实例,没有懒加载,一般除非客户端调用该getInstance方法,否则我们应该避免实例化;

   ​ 这种方式不提供任何的异常处理。

2.3.2、饿汉式(静态代码块,线程安全)

    ​静态代码块的方式类似于静态常量。只是类的实例是在提供异常处理选项的静态块中创建的。

class SingletonEagerStatic
 private static SingletonEagerStatic instance;
 
 //私有构造函数避免客户端应用程序使用构造函数
 private SingletonEagerStatic()
 
 static
        try
            instance = new SingletonEagerStatic();
        catch(Exception e)
            throw new RuntimeException("Exception occured in creating singleton instance");
        
    
 
 public static SingletonEagerStatic getInstance()
        return instance;
    

2.3.3、懒汉式(线程不安全)

public class SingletonLazyTest 

 public static void main(String[] args) 
  SingletonLazy instance1 = SingletonLazy.getInstance();
  SingletonLazy instance2 = SingletonLazy.getInstance();
  System.out.println(instance1==instance2); //true
 


/**
 * 懒汉式(线程不安全)
 */
class SingletonLazy
    private static SingletonLazy instance;
    
    private SingletonLazy()
    
    public static SingletonLazy getInstance()
     //如果多个线程同时在 if 条件内,则可能会导致问题。
        if(instance == null)
            instance = new SingletonLazy();
        
        return instance;
    

  • 优点

   ​ 实现了懒加载,即调用getInstance方法时才创建对象,但只适合在单线程环境下运行。

  • 缺点

   ​ 如果多个线程同时在 if 条件内,则可能会破坏单例模式,两个线程将获得不同实例。

   ​ 我们写个案例来测试一下:

public class SingletonLazyTest 

 public static void main(String[] args) 
  for (int i = 0; i < 4; i++) 
   Thread t1 = new Thread(new SingletonThread());
          t1.start();
  
 


/**
 * 懒汉式(线程不安全)
 */
class SingletonLazy
    private static SingletonLazy instance;
    
    private SingletonLazy()
    
    public static SingletonLazy getInstance()
     //如果多个线程同时在 if 条件内,则可能会导致问题。
        if(instance == null)
            instance = new SingletonLazy();
        
        return instance;
    


class SingletonThread implements Runnable
 @Override
 public void run() 
  SingletonLazy instance1 = SingletonLazy.getInstance();
  System.out.println(Thread.currentThread().getName() + ":" + instance1);
 

​    我用四个线程去执行getInstance方法,多次运行时,就可能存在不同的实例对象。结果如下:

扩展:多线程需要处理的问题

    这里简单的说下原因:

   ​ 随着技术的发展,CPU的运算速度也突飞猛进,与内存(主存)、磁盘的访问速度逐减拉开了距离。除了提升频率,后来甚至引入了L1、L2、L3三级缓存。

   ​ 电脑访问磁盘的一个过程:

​    在存储层面,速度最快的是 CPU 中的寄存器(小于1kb),CPU比内存速度快很多,由 CPU 直接访问内存效率较低,为了提高内存访问速度,在 CPU 和内存间增加了高速缓存(高速小容量存储器)。

   ​ 寄存器:种类挺多,CPU访问寄存器几乎没有延迟,但是容量很小。早期的计算机用的是8位寄存器,到后来16位、32位、64位,简单的理解就是单位时间内能处理二进制数据的长度。

   ​ 在执行程序时为了提高性能,编译器和CPU常常会对指令做重排序。指令队列在CPU执行时不是串行的, 当某条指令执行时消耗较多时间时, CPU资源足够时并不会在此无意义的等待,而是开启下一个指令,开启下一条指令是有条件的, 即上一条指令和下一条指令不存在相关性。

   ​ 同样JVM 允许在不影响代码最终结果的情况下,可以乱序执行。但是在单线程环境的执行结果是不能被改变的,所以我们无需担心重排顺序会干扰到他们。

   ​ 在《深入理解Java虚拟机》中,描述到:

   ​ JVM(Java Virtual Machine,Java虚拟机):是一种抽象的计算机,基于堆栈架构,它有自己的指令集和内存管理,它加载 class 文件,分析、解释并执行字节码。JVM 主要分为三个子系统:类加载器运行时数据区执行引擎。java虚拟机规范定义了java内存结构和内存模型,用于屏蔽各种硬件和操作系统的内存访问差异,以实现跨平台的效果。

  • 类加载器

   ​ 主要功能是处理类的动态加载,还有链接,并且在第一次引用类时进行初始化

  • 运行时数据区

   ​ 它约定了在运行时程序代码的数据比如变量、参数等等的存储位置。主要包括:堆、java栈、方法区、pc寄存器和本地方法栈。

   ​ 1)堆:线程共享,存放类实例对象和数组。堆所占内存的大小由-Xmx指令和-Xms指令来调节。

   ​ 2)栈:线程私有,存储局部变量表、操作栈、动态链接、方法出口,对象指针,JVM只会对java栈执行两种操作压栈或出栈。

   ​ 3)方法区:线程共享,存储被装载类型的信息;

   ​ JDK1.6及之前运行时常量池逻辑包含字符串常量池存放在方法区;

   ​ JDK1.7 字符串常量池被从方法区拿到了堆中,运行时常量池剩下的东西还在方法区;

   ​ JDK1.8 移除了永久代用元空间取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(堆外内存)。

   ​ 4)pc寄存器:保存有当前正在执行的JVM指令的地址;

   ​ 5)本地方法栈:服务于 Native 方法;

  • 执行引擎

   ​ 运行时数据区存储着要执行的字节码,执行引擎将会读取并逐个执行。包括:解释器JIT编译器GC垃圾收集器

   ​ 在java中每个线程创建时JVM都会为其创建一个工作内存(即栈空间),工作内存是每个线程的私有数据区域,而共享变量则都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。

   ​ 如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。为了解决这个问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则就是JMM(Java Memory Model Java,java内存模型),它就是用来描述多线程如何正确的通过内存进行交互和使用共享数据。

   ​ 在JMM中,有两条规定:

   ​ 1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;

   ​ 2)不同线程之间无法访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

   ​ JMM整体是围绕着程序执行的原子性、有序性、可见性展开的。

  • 原子性

   ​ 原子性是指是一个操作是不可中断的,即多线程环境下两个线程间的操作互不干扰。JMM规定了内存交互操作有8种。其中read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)六个指令直接提供原子操作。我们可以认为java基本数据类型的变量(除了long和double)的读写操作都是原子的。对于lock(锁定)、unlock(解锁),虚拟机没有将操作直接开放给用户使用,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者Lock锁接口的实现类来保证程序执行的原子性。volatile不具备原子性。

​ 例如:线程对主内存的读操作:

   ​ read可能远早于use, 在它们中间可能会发生的其他线程的读写。所以在多线程情况下,可能发生线程安全问题。

  • 可见性

       可见性就是指当一个线程修改了共享变量值时,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量这种依赖主内存作为传递媒介的方式来实现可见性。其中被volatile修饰的变量,可以在修改后强制刷新到主存,并在使用时从主存获取刷新,普通变量则不行。除了volatile修饰的变量,Java还有两个关键字能实现可见性,他们是synchronized和final。

  • 有序性

   ​ 在执行程序时为了提供性能,做了指令重排。java提供了volatile和synchronized两个关键字保证线程之间的有序性。

   ​ 当然频繁的加锁和使用volatile来解决多线程安全问题,势必会影响性能,所以,其实JMM中还为我们提供了happens-before原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它可能判断线程是否安全。

2.3.4、懒汉式(同步方法,线程安全)

/**
 * 懒汉式(线程安全,同步方法)
 */
class SingletonLazySafe
    private static SingletonLazySafe instance;
    
    private SingletonLazySafe()
    
    public static synchronized SingletonLazySafe getInstance()
        if(instance == null)
            instance = new SingletonLazySafe();
        
        return instance;
    

  • 优点

   ​ 为懒加载,解决了线程安全问题;

  • 缺点

   ​ 由于使用了线程同步方法,使得每次线程进来都要进行方法同步,降低了性能。静态方法是类锁,synchronized关键字修饰方法(静态方法、对象方法)通过ACC_SYNCHRONIZED 标示符来实现方法同步。

2.3.5、懒汉式(同步代码块,线程不安全)

/**
 * 懒汉式(线程不安全,代码块同步)
 */
class SingletonLazySafe
    private static SingletonLazySafe instance;
    
    private SingletonLazySafe()
    
    public static SingletonLazySafe getInstance()
        if(instance == null)
         synchronized(SingletonLazySafe.class)
          instance = new SingletonLazySafe();
         
        
        return instance;
    

   ​ 为什么代码块同步,这样写达不到效果呢。同步代码块(锁类、锁对象)主要通过monitorenter(对monitor+1)和monitorexit(对monitor-1)指令来实现,每个对象都有一个自己的monitor(监视器锁),通过指令来进入监视器获取锁和释放锁。而instance = new SingletonLazySafe();创建对象的过程(分配内存空间、初始化对象、分配内存指针,也会存在指令重排)并不是原子的,当另外一个线程在还没拿到instance的指针的时候就进入了if(instance == null),那同样会出现单例类存在多个对象。

2.3.6、双重检查(线程安全)

/**
 * 懒汉式(线程安全,双重检查)
 */
class SingletonLazySafe
    //当然还有人给变量加volatile,进一步保证线程修改instance后立即刷新主内存
    private static SingletonLazySafe instance;
    
    private SingletonLazySafe()
    
    public static SingletonLazySafe getInstance()
     //减少开销
        if(instance == null)
         synchronized(SingletonLazySafe.class)
                 //防止其他线程进了第一个if造成线程安全问题
          if(instance == null)
           instance = new SingletonLazySafe();
          
         
        
        return instance;
    

   ​ 双重检查是线程安全的,synchronized的两种方式都可以实现线程同步问题。

   ​ 其实在jdk1.6之前,synchronized锁会调用底层的操作系统实现(互斥原语mutex),这会频繁的切换CPU的状态,效率比较低,因此也叫重量级锁。所以oracle官方在jdk1.6之后对synchronized锁进行了升级。升级之后主要存在四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

2.3.7、静态内部类(线程安全)

/**
 * 静态内部类(线程安全)
 */
class BillPughSingleton 

    private BillPughSingleton()
    //加载单例类时,SingletonHelper类并不会加载到内存中
    private static class SingletonHelper
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    
    
    public static BillPughSingleton getInstance()
        return SingletonHelper.INSTANCE;
    

  • 优点

   ​ 在jdk1.5之前java内存模型并不是那么完美。为了解决线程安全问题,Bill Pugh 提出了一种不同的方法来使用内部静态辅助类创建Singleton 类。即达到了线程安全的目的,又实现了懒加载。所以以前这种写法也比较多。

2.3.8、枚举(线程安全)

/**
 * 枚举(线程安全)
 */
enum EnumSingleton  
 //反编译后的枚举其实是个final类,且枚举项都是static的
 INSTANCE;
 public static EnumSingleton getInstance()
        return INSTANCE;
    

  • 优点

   ​ jdk1.5中新增了枚举,枚举值是全局访问的,是线程安全的;

  • 缺点

   ​ 枚举类型有些不灵活,也没有实现懒加载。

2.4、总结及应用场景

   ​ 上面总结了很多单例的实现方式。为什么要延迟实例化单例对象:

   ​ 1)在静态初始化时,没有足够的信息对单例对象进行初始化。例如:工厂单例就必须等待真正的工厂机器,才能建立通信通道;

   ​ 2)选择延迟初始化单例对象与获取资源有关,例如:数据库连接,尤其是在一个特定的会话中,它包含的应用程序并不需要该单例对象;

​    单例模式的应用场景:

   ​ 1)需要频繁实例化或被共享的场合。比如:日志记录、缓存和线程池;

   ​ 2)控制硬件级别的操作。比如:驱动程序对象

   ​ 3)单例模式也可以用于其他设计模式:比如抽象工厂模式、建造者模式、原型模式即门面模式都可以作为单例实现。

以上是关于重学设计模式(三设计模式-单例模式)的主要内容,如果未能解决你的问题,请参考以下文章

重学设计模式(三设计模式-组合模式)

重学设计模式(三设计模式-原型模式)

重学设计模式(三设计模式-迭代器模式)

重学设计模式(三设计模式-迭代器模式)

重学设计模式(三设计模式-责任链模式)

重学设计模式(三设计模式-责任链模式)