signed

QiShunwang

“诚信为本、客户至上”

java常见设计模式之---单例模式

2020/12/28 22:59:54   来源:

java常见设计模式之---单例模式

  • 1、单例模式简介
    • 应用场景举例
  • 2、单例模式的特点
  • 3、单例模式和静态类
  • 4、单例模式的经典实现
    • 饿汉式单例(典型实现)
      • 饿汉式-静态代码块
    • 懒汉式单例创建,五种方法
      • 0)、懒汉式(典型实现)
      • 1)、同步延迟加载 — synchronized方法
      • 2)、双重检验锁模式(double checked locking pattern)
        • 双重检验锁-volatile关键字防止重排序
      • 3)、静态内部类
      • 4)、lock机制
      • 5)、枚举法

1、单例模式简介

单例模式(Singleton Pattern)是一个比较简单的模式,其原始定义如下:Ensure a class has only one instance, and provide a global point of access to it. 即确保只有一个实例,而且自行实例化并向整个系统提供这个实例。

因程序需要,有时我们只需要某个类只保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。事实上,这些应用都或多或少具有资源管理器的功能。

总之,选择单例模式就是为了避免不一致状态。

应用场景举例

1)、spring中的单例模式:在spring中每个bean都是单例的,这样做的好处是Spring容器可以管理这些bean的生命周期,由spring容器来决定实例的创建、销毁、销毁后的处理等等问题。如果采用非单例模式,则Bean初始化后的管理交给J2EE容器了,Spring容器就不在跟踪管理Bean的生命周期了。

2)、多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。。

3)、操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

4)、应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加

5)、Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源

2、单例模式的特点

包括下面几个部分的简单记忆”3125

3和1一起记忆,即三个一

  • 单例模式只能有一个实例,
  • 单例类必须创建自己的唯一实例
  • 单例类必须向其他对象提供这一实例

综上所述,单例模式就是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种方法。

2即是指2种实现方式:懒汉式和 饿汉式
5即是指五种经典实现

有两种场景可能导致非单例的情况

场景一:如果单例由不同的类加载器加载,那便有可能存在多个单例类的实例

场景二:如果 Singleton 实现了 java.io.Serializable 接口,那么这个类的实例就可能被序列化和反序列化。

3、单例模式和静态类

静态类也可以实现一个类只有一个对象,这又和单例模式有什么区别呢?

  1. 单例可以继承和被继承,方法可以被重写(override),但是静态方法不行。
  2. 静态方法中产生的对象会在执行后被释放,进而被GC所清理,不会一直存在于内存中。
  3. 静态类会在第一次运行时初始化(饿汉式也这样),单例模式可以有其他选择,如立即加载(饿汉式)和延迟加载(懒汉式)
  4. 基于上面两条,单例模式往往存在于DAO层,如果反复的初始化和释放会占用很多系统资源,而使用单例模式将其加载于内存中可以节省资源开销。
  5. 单例模式容易测试

立即加载 : 在类加载初始化的时候就主动创建实例;
延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。

静态类和单例模式情景的选择:

情景一:不需要维持任何状态,仅仅用于全局访问,此时更适合使用静态类。

情景二:需要维持一些特定的状态,此时更适合使用单例模式。

4、单例模式的经典实现

饿汉式单例(典型实现)

想想“三个一”特点
饿汉式单例实现的方式如下,这种方法是线程安全的,因为单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

//饿汉式单例实现
public class Singleton1 {
    private Singleton1(){}
    private static Singleton1 singleton1 = new Singleton1();
    public static Singleton1 getInstance (){
        return singleton1;
    }
}

**缺点:**因为是立即加载的方式创建单例,所以在单例无用的情况下回造成资源的浪费,大量饿汉式单例会给系统造成极大的开销。

饿汉式-静态代码块

public class Singleton {
   private Singleton instance = null;
   private Singleton() {}
	// 初始化顺序:基静态、子静态 -> 基实例代码块、基构造 -> 子实例代码块、子构造
   static {
       instance = new Singleton();
  }

   public static Singleton getInstance() {
       return this.instance;
  }
}

类初始化时实例化 instance

懒汉式单例创建,五种方法

想想“3125”中的二和五

0)、懒汉式(典型实现)

//不完美的饿汉式单例
public class Singleton2 {
    private Singleton2(){}
    private static Singleton2 instance = null;
    public static Singleton2 getInstance (){
        if (instance == null){
            instance = new Singleton2();
        }
        return instance ;
    }
}

为什么说上面的饿汉式单例不完美呢,在单线程情况下,这种方法是线程安全的,但假设在多线程情况下
多个线程会同时执行到if (instance == null),此时尚未有线程创建出instance = new Singleton2();,这时就会有多个线程进入到if判断语句中,创建出多个对象。

这里介绍个知识点,在下面也有用到:

当我们写了 new 操作,JVM 到底会发生什么?
首先,我们要明白的是,new Singleton()是一个非原子操作,代码行instance = new Singleton2();的执行过程可以分为下面三步:
(1)、memory = allocate(); //先在内存开辟空间
(2)、ctorInstance(memory); //初始化对象
(3)、instance = memory; //使instance 指向刚分配的内存地址(执行完这步 instance 才是非Null的)

要留意的是:这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:
(1)、memory = allocate(); //先在内存开辟空间
(2)、instance = memory; //使instance 指向刚分配的内存地址
(3)、ctorInstance(memory); //初始化对象

1)、同步延迟加载 — synchronized方法

public class Singleton3 {
    private Singleton3(){}
    private static Singleton3 instance = null;
    public static synchronized Singleton3 getInstance (){
        if (instance == null){
            instance = new Singleton3();
        }
        return instance ;
    }
}

这种方法确实做到了线程安全,但是在每一个线程执行到getInstance()方法时,只有一个线程获得锁,其他线程需要等待。这样就会因为等待锁资源造成系统性能的下降。

上面传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton3()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。

2)、双重检验锁模式(double checked locking pattern)

对上面的方法进行改进

进行两次 instance == null? 的判断,如果为true才能进入同步代码块,而在同步代码块中再检测一次是因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就有可能会生成多个实例了。

//同步延迟加载 — synchronized块
public class Singleton4 {
    private Singleton4(){}
    private static Singleton4 instance= null;
    public static Singleton4 getInstance(){
    	if (instance == null){
        // 使用 synchronized 块,临界资源的同步互斥访问
        synchronized (Singleton4.class){
                if (instance == null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

为什么说使用synchronized块是线程不安全的呢,这里就需要用到前面提到的前置知识了。

如果intence = new Singleton4();的执行顺序是1-2-3
即:先分配内存空间,再初始化成员变量,最后分配引用地址。这样出来的instance 是非null的。

但如果执行顺序是1-3-2的情况下
即:先分配内存空间,在分配引用地址,最后再初始化变量。

如果是后者,则在 3 执行完毕、2 未执行之前,此时另外有一个线程进行第一个判断,发现instance!=null,返回instance(但其实instance还没有完成初始化),使用会导致报错,当然发生错误的概率极低,但是这个错误我们是可以规避的。

只需要将 instance 变量声明成 volatile 就可以了。

双重检验锁-volatile关键字防止重排序

public class Singleton4 {
    private Singleton4(){}
    //给instance加上了volatile关键字
    private volatile static Singleton4 instance= null;
    public static Singleton4 getInstance(){
            if (instance == null){
            // 使用 synchronized 块,临界资源的同步互斥访问
            synchronized (Singleton4.class){
                if (instance == null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

但是特别注意
在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

3)、静态内部类

public class Singleton {  
//静态内部类里面创建了一个Singleton单例
   private static class InstanceHolder {  
      private static final Singleton INSTANCE = new Singleton();  
  }  
   private Singleton (){}  
   public static final Singleton getInstance() {  
      return InstanceHolder.INSTANCE;  
  }  
}  

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

4)、lock机制

// 类似双重校验锁写法
public class Singleton {
      private static Singleton instance = null;
      private static Lock lock = new ReentrantLock();
      private Singleton() {}
      public static Singleton getInstance() {
          if(instance == null) {
              lock.lock(); // 显式调用,手动加锁
              if(instance == null) {
                  instance = new Singleton();
              }
              lock.unlock(); // 显式调用,手动解锁
          }
          return instance;
      }
}

5)、枚举法

public enum singleton5 {
	INSTANCE;
	
	private  InnerClass _instance;
	
	private singleton5() {
		_instance = new InnerClass();
	}
	
	public InnerClass getInstance() {
		return _instance;
	}
	
	private class InnerClass{
		
	}
	
}

主要是枚举类型是值类型的数据,所以不可能获取到构造函数,也就无法通过构造函数来得到新的实例。这样可以实现很安全的单例模式。(目前很多使用枚举实现单例模式)

缺点:
枚举类型会造成更多的内存消耗。枚举会比使用静态变量多消耗两倍的内存,如果是Android应用,尽量避免。

特点:
(1)线程安全(枚举类型默认就是安全的)

(2)避免反序列化破坏单例