定义:在当前进程中,通过单例模式创建的类有且只有一个实例

单例模式有以下几个特点:

  • 在java 应用中,单例模式能保证在一个 jvm 中,该对象只有一个实例存在
  • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
  • 没有公开的set 方法,外部类无法调用 set方法创建实例
  • 提供一个公开的 get 方法获取唯一的实例

该模式的几个好处:

  • 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的开销
  • 省去了 new 操作符,降低了系统内存使用频率,减轻 GC 压力
  • 系统中某些类,比如 controller,控制着处理流程,如果该类创建多个的话,系统完全乱了
  • 避免对资源的重复占用

几种写法

饿汉:提前把对象 new 出来,即使是别人第一次调用get 方法获取实例,也可以直接或得到,不需要创建这一步

适用场景:经常访问的热点数据,那么系统启动的时候就用饿汉模式提前加载(类似缓存的预热),这样哪怕是第一个用户调用都不会存在创建开销,而且频繁的调用也不存在内存浪费

饿汉式
public class Singleton {
  // 创建一个实例对象
    private static Singleton instance = new Singleton();
    /**
     * 私有构造方法,防止被实例化
     */
    private Singleton(){}
    /**
     * 静态get方法
     */
    public static Singleton getInstance(){
        return instance;
    }
}

懒汉:调用的时候再创建

适用场景:不常用的数据,如果不确定某个数据是否有人调用,那就用懒汉,如果使用了饿汉,但是又没人使用,提前加载的类在内存中有资源浪费的。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

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

懒汉与饿汉的区别:第一次创建时的开销问题,以及线程安全问题

线程安全问题:上面的懒汉有线程安全问题

图片

在运行过程中可能存在这么一种情况:多个线程去调用getInstance方法来获取Singleton的实例,那么就有可能发生这样一种情况,当第一个线程在执行if(instance==null)时,此时instance是为null的进入语句。

在还没有执行instance=new Singleton()时(此时instance是为null的)第二个线程也进入了if(instance==null)这个语句,因为之前进入这个语句的线程中还没有执行instance=new Singleton(),所以它会执行instance = new Singleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。

这样就导致了实例化了两个Singleton对象,简单的解决方式就是加锁,每次get 时先锁起来,效率低

直接加锁
public class Singleton {
    private static Singleton instance = null;
    /**
     * 私有构造方法,防止被实例化
     */
    private Singleton(){}
    /**
     * 静态get方法
     */
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

升级一点的处理方式就是 doublecheck,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升,但还是有问题(指令重排)

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        //先检查实例是否存在,如果不存在才进入下面的同步块
        if(instance == null){
            //同步块,线程安全的创建实例
            synchronized (Singleton.class) {
                //再次检查实例是否存在,如果不存在才真正的创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

问题就在于指令重排,创建一个对象大致分为三步:1分配空间,2初始化对象信息,3返回对象的地址给引用;

doublecheck依旧无法避免指令重排,理想顺序是 123,实际顺序可能是 132,下面以 A、B 两个线程为例:

  1. A、B线程同时进入第一个 if 判断
  2. A 首先进入synchronized块,由于instance 为 null,所以它执行instance = new Singleton();
  3. 由于JVM 内部的优化机制(指令重排),先分配了空间(这一步没问题),赋值给了instance 成员(这一步有问题,此时还没有初始化该实例),然后 A 离开了synchronized 代码块
  4. B 进入 sync 代码块,由于 instance 此时不是 null,因此离开马上sync 代码块,并吧未初始化的 instance 返回
  5. 此时B使用instance,因为该对象未初始化,因此报错

为了处理这个问题(指令重排导致的 doublecheck 线程不安全问题),加上了 volatile

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        //先检查实例是否存在,如果不存在才进入下面的同步块
        if(instance == null){
            //同步块,线程安全的创建实例
            synchronized (Singleton.class) {
                //再次检查实例是否存在,如果不存在才真正的创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

截止目前(加上 volatile 的 doublecheck),其实还要可以优化的空间,因为 volatile 会屏蔽一些 jvm 必要的代码优化,因此并不能达到完美

使用内部类来维护单例的实现,

public class Singleton {  

    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  

    /* 此处使用一个内部类来维护单例 */  
    private static class SingletonFactory {  
        private static Singleton instance = new Singleton();  
    }  

    /* 获取实例 */  
    public static Singleton getInstance() {  
        return SingletonFactory.instance;  
    }  

    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  
}

jvm 内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。

这样当第一次调动get 的时候,JVM 能够保证instance 只被创建一次,并且会保证把复制给instance的内存初始化完毕。

该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题

终结写法

public enum Singleton {
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例。
     */
    Instance;
}

静态方法与单例模式的选择

两者都可以实现加载的最终目的,但两个一个是基于对象,一个是面向对象的,不用面向对象也能解决问题,面向对象的代码提供了一个更好的编程思想。

如果一个方法和他所在类的实例对象无关,那么它就应该是静态的,反之他就应该是非静态的。如果确实应该使用非静态的方法,但是在创建类时又确实需要维护一份实例时,就应该用单例

比如电商中就有很多类,有很多配置和属性,这些配置和属性是一定存在的,又是公共的,同时需要在整个生命周期中存在,所以只需要一份就行。

当用单例或者静态方法去维护这一份值且只有这一份值,但此时这些配置和属性又是通过面向对象的编码得到的,那就应该使用单例模式;或者不是面向对象的,但他本身的属性应该是面向对象的,也最好用单例

results matching ""

    No results matching ""