Java中的单例模式(Singleton Pattern in Java)

Introduction

单例模式在很多的框架中被广泛使用。

对于系统中的某个类来说,只有一个实例是很重要的,比如只能有一个timer和ID Producer。又比如在服务器程序中,配置信息保留在一个文件中,这些配置信息只由一个单例对象统一获取,进程中的其他对象通过这个单例对象获取这些配置信息,通过这种方式大大简化复杂环境下的配置管理。

这个时候一个类里面就只能有一个实例,并且这个实例要易于访问。我当然可以只定义一个全局变量从而保证对象随时都能访问。但是如果是这种方式来实现单例模式的话,依然可以实例化多个instance,而且被不同的对象所持有(这就违反了单例模式的条件了)不是很妙。

实现单例模式的思路是:

一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

如果类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。

Definition

Singleton Pattern:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

  1. 单例类只能由一个实例
  2. 自行创建实例
  3. 自行向整个系统(所有其他对象)提供此实例

Attention

在多线程的应用场合中使用单例模式的时候要注意。如果唯一的单例尚未被创建(懒汉模式),有两个线程同时调用构建的方法,他俩都不会检测到唯一实例,于是每个线程都创建了一个实例,这就违反了单例模式中实例唯一的原则。

所以我们可以为指示类是否实例化的变量提供一个互斥锁。(会降低效率)

Java实现

常用的构建方式

  • 懒汉模式。指全局的单例模式在第一次使用时被创建。
  • 饿汉方式。指全局的单例模式在类装载的时候被创建。

Example

  1. 饿汉模式
public class Sington{
     private static Singleton instance = new Singleton();  
     private Singleton (){
     }
     public static Singleton getInstance() {  
     return instance;  
     }  
}

在类加载的时候完成初始化,所以类的加载速度会比较慢,但是可以很快的获取对象,优势在于不需要去考虑多线程的同步问题。

  1. 懒汉模式

    2.1 lock

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

    这样来加锁是一个比较简单粗暴的方式,但是比较无脑synchronized,每次调用getInstance都会进行一次同步,会有一定的性能消耗,实际上我只需要第一次初始化的时候加锁就好了。

    2.2 double checked locking

public class Singleton {
    private static volatile Singleton instance = null;
  
    // Private constructor suppresses 
    // default public constructor
    private Singleton() {};
  
    //Thread safe and performance  promote 
    public static  Singleton getInstance() {
        if(instance == null){
             synchronized(Singleton.class){
                 // When more than two threads run into the first null check same time, 
                 // to avoid instanced more than one time, it needs to be checked again.
                 if(instance == null){ 
                     instance = new Singleton();
                  }
              } 
        }
        return instance;
    }
  }

双重检查锁的优势在于它先判断了对象是否已经初始化,再决定要不要加锁。

使用了volatile关键字之后,所有的写操作发生在读操作之前。这个解决方案需要 JDK5 或更高版本(因为从 JDK5 开始使用新的 JSR-133 内存模型规范,这个规范增强了 volatile 的语义)

  1. Initialization On Demand Holder idiom

JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

public class Singleton { 
    public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance ;  // 这里将导致 InstanceHolder 类被初始化 (只有第一次调用getInstance方法的时候,虚拟机加载InstanceHolder并且初始化instance)
    }
}
} 

不是很清楚这种基于类初始化的方案和上面的双重检查模式到底谁的性能更好。

但基于 volatile 的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

参考


单例模式

双重检查锁定与延迟初始化

posted @ 2019-07-22 11:48 天天不是小可爱 阅读(...) 评论(...) 编辑 收藏