设计模式--单例模式

简介

单例模式是一种创建型设计模式,能够保证一个类只有一个实例,并提供一个访问该实例的全局方法

识别方法:单例实例可以通过返回相同缓存对象的静态方法来识别

缺点:违反了单一职责原则(因为单例模式同时解决了两个问题:保证一个类只有一个实例、为该类提供全局的访问方法)


实现思路

  1. 将默认构造函数私有化,防止其它对象使用单例类的 new 运算符
  2. 新建一个静态方法调用私有的构造函数进行创建对象,并将该实例对象保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象

代码实现

饿汉式

所谓饿汉式就是直接创建出类的实例化对象(可直接实例化或者通过静态代码块初始化),然后用private私有化,对外只用静态方法暴露

  • 实例属性可以通过配置文件进行注入
public class Singleton{
    
    private static final Singleton singleton = new Singleton();
    
    /**
    static{
        singleton = new Singleton();
    }
   */
    
    private Singleton(){
    
    }
    
    public static Singleton getInstance(){
        return singleton;
    }
}

缺点:类加载时就初始化,若未使用则浪费内存空间

优点:在并发的情况下是线程安全的


懒汉式

所谓的懒汉式就是在需要该单例模式才调用私有构造函数进行初始化

缺点:必须通过加锁的方式保证多线程下的单例,但加锁会影响效率

优点:只在第一次调用才会进行初始化,避免了内存浪费

线程不安全模式

这种方式只能在单线程下使用,多线程下会不安全,因为当多个线程并发同时判断 singleton 是否为空时,就会产生多个相应的实例化对象

public class Singleton{

    private static Singleton singleton;
    
    private final String name;
    
    private Singleton(String name){
       this.name = name;
    }
    
    public static Singleton getInstance(String name){
    
        if(singleton == null){
            return new Singleton(name);
        }
        
        return singleton;
    }
    
    
}

线程安全模式

public class Singleton{
    
    private static volatile Singleton singleton;
    
    private final String name;
    
    private Singleton(String name){
        this.name = name;
    }
    
    public static Singleton getInstance(String name){
        if(singleton == null){
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton(name);
                }
            }
        }
        
        return singleton;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

声明变量时为什么要用 volatile : private static volatile Singleton singleton;

volatile 关键字可以防止JVM指令重排优化,使用了 volatile关键字 可以用来保证其线程之间的可见性和有序性

  • 防止JVM指令重排优化
    • singleton = new Singleton(); 实例的创建可以分为三步

      1. 为 singleton 分配内存空间

      2. 初始化 singleton

      3. 将 singleton 指向分配的内存空间

      但由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。指令重排的单线程下不会出现问题,但在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1 执行了 步骤1和3,此时 线程T2 调用 getInstance() 后发现 singleton 不为空,因此返回 singleton ,但是此时的 singleton 还未被初始化。

      使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行

  • 保证变量在多线程运行时的可见性
    • 在Java内存模型下,线程可以将变量保存到本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另一个线程还在继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。要解决这个问题,就需要把变量声明为 volatile,告诉JVM,这个变量是不稳定的,每次使用这个变量时都到主存中进行读取

进行了两次 singleton == null 校验作用

  • 第一次判断 singleton 是否为 null
    • 第一次判断是在 synchronized 同步代码快外,理由是单例模式只会创建一个实例,并通过 getInstance() 方法返回 singleton 对象,所以如果已经创建了 singleton 对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样可以提升效率
  • 第二次判断 singleton 是否为 null
    • 第二次判断是为了保证同步;假如 线程T1 通过了第一次判断,进入了同步代码块层,线程T2 阻塞等待。当 线程T1 执行完后释放锁对象,线程T2 就进入同步代码块,假如这时不判断,就会存在:线程T2 也创建了实例,就会把 线程T1 创建的实例覆盖,就会造成多实例的情况

Java 中使用案例

Java 核心程序库中仍有相当多的单例示例

  • java.lang.Runtime#getRuntime()

识别方法 单例可以通过返回相同缓存对象的静态构建方法来识别

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}
posted @ 2022-08-31 14:46  伊文小哥  阅读(19)  评论(0编辑  收藏  举报