关注「Java视界」公众号,获取更多技术干货

单例模式(单例模式作用、常见形式、代码实现)

一、单例模式能干啥?

所谓单例,就是整个程序有且仅有一个实例。

某个类全局只有一个实例对象有什么好处?一方面,由于单例模式只生成一个实例,减少了系统性能开销;另一方面,单例模式存在全局访问点,所以可以优化共享资源访问。比如:网站的计数器,一般也是采用单例模式实现,如果存在多个计数器对象,每一个用户的访问都刷新不同的计数器对象的值,统计总数的时候就很麻烦。如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。还有很多这样的场景:

  1. Windows的任务管理器
  2. Windows的回收站,也是一个单例应用
  3. 项目中的读取配置文件的对象(如Mybatis中的Configuration类的对象,保存配置信息)
  4. 数据库的连接池
  5. Servlet中的Application Servlet
  6. Spring中的Bean默认也是单例的
  7. SpringMVC Struts中的控制器

总的来说适用于:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 
  • 4.方便资源相互通信的环境

二、单例模式常见形式

总的按对象的创建时机,可以分为懒汉式和饿汉式:

1.饿汉式:线程安全 调用率高 但是不能延迟加载
2.懒汉式:线程安全 调用率不高 但是可以延迟加载
3.双重检测(double check )-----懒汉式
4.静态内部类(线程安全 可以延迟加载)--------懒汉式
5.枚举单例 线程安全 不可以延迟加载

看着形式很多,实际上单例模式都有以下特点:

  1. 构造器私有:这个很好理解,要是构造器不私有,那就可以在外部随意创建不同的对象了,违背了单例思想
  2. 持有自己类型的属性
  3. 对外提供获取实例的静态方法

三、懒汉式

懒汉式顾名思义,比较懒,只在你需要的时候才创建这个唯一的对象。它分为两种:

懒汉式(1)

public class Singleton1 {
    // 自己持有自己
    private static Singleton1 instance;
    // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
    private Singleton1() {}
    // 对外提供获取唯一实例的静态方法
    public static Singleton1 getInstance() {
        // 先判断实例对象是否已经存在
        if(instance == null){
            // 创建实例
            instance = new Singleton1();
        }
        return instance;
    }
}

上面的如果是单线程的情况下是没问题的,但是在多线程时就有可能产生多个不同的对象,即是线程不安全的。

懒汉式(2)

上面懒汉(1)线程不安全,因此在创建实例方法上加上锁就有了下面的

public class Singleton2 {
    // 自己持有自己
    private static Singleton2 instance;
    // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
    private Singleton2() {}
    // 对外提供获取唯一实例的静态方法
    public static synchronized Singleton2 getInstance() {
        // 先判断实例对象是否已经存在
        if(instance == null){
            // 创建实例
            instance = new Singleton2();
        }
        return instance;
    }
}

因为锁的原因,效率自然不高。 

四、饿汉式

 饿汉式顾名思义,比较饥渴,不管你要不要这个对象都先给你创建出来。

public class Singleton3 {
    // 自己持有自己并直接创建对象
    private static Singleton3 instance = new Singleton3();
    // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
    private Singleton3() {}
    // 对外提供获取唯一实例的静态方法
    public static Singleton3 getInstance() {
       return instance;
    }
}

和懒汉式的延时创建相比,饿汉式在加载类的时候对象就已经创建了,所以加载类的速度比较慢,但是获取对象的速度比较快,调用效率高,且是线程安全的。

五、双检锁式(DCL)

上面的懒汉式(2)加了锁虽然线程安全了,但是效率也降低了,而双检锁式可以兼顾线程安全和效率。实际上,双检锁也可以看成是懒汉式的一种特殊形式,也是延时创建唯一的对象。

public class Singleton4 {
    // 自己持有自己并直接创建对象(使用volatile关键字防止重排序,new Instance()是一个非原子操作,可能创建一个不完整的实例)
    private static volatile Singleton4 instance;
    // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
    private Singleton4() {}
    // 对外提供获取唯一实例的静态方法
    public static Singleton4 getInstance() {
        // 判断是否存在单例
        if(instance == null){
            // 加锁,保持只有一个线程执行(只需在第一次创建实例时才同步)
            synchronized (Singleton4.class){
                // 再次判断单例是否被创建(防止其他线程已经创建而导致再次创建)
                if (instance == null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

这里有两次对象的判空处理,锁是在两次判空中间的,这个锁只会在第一次创建实例的时候执行一次,一旦该实例被创建后面的线程就不会再需要拿到这个锁,因此不会因为锁而降低效率。

注意:

这里还有个关键字volatile,它是来防止某个线程获取不完整对象的。new Instance()是一个非原子操作,jvm存在乱序执行功能,因为重排序的原因,可能创建出来的就是一个不完整的实例。比如演示一下new Instance()过程:

  1. 分配实例所需的内存
  2. 创建引用,指向这个内存
  3. 初始化实例对象

这是一种顺序,但这个顺序不是固定的,实际过程中也可能是下面的顺序

  1. 分配实例所需的内存
  2. 初始化实例对象
  3. 创建引用,指向这个内存

每次执行顺序是重排序的,那就可能发生下面的现象:

  1. 线程A先进入Singleton4()方法
  2. 此时instance == null,线程A进入synchronized 块
  3. 再次判断,此时instance == null,线程A在执行 instance = new Singleton4();时,先执行“分配实例所需的内存”和“创建引用指向这个内存”,但还没有初始化这个对象
  4. 此时线程B进来,因为instance虽然未被初始化,但已经非null,不会再进行下面的创建语句,直接返回这个未初始化的instance
  5. 线程A等到资源后,继续完成对象初始化操作,线程A获得完成的instance对象

上面的线程B就拿到了非完整对象,这应该是个重大bug。

volatile包含以下语义:

(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个valatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

volatile 关键字修饰这个对象可以避免以上重排序可能带来的问题(volatile 需要在JDK1.5之后的版本才能确保安全)。

六、静态内部类式 

/**
 * Feng, Ge 2020/2/29 14:14
 */
public class Singleton5 {
    // 构造器私有化,不让外部通过构造器产生对象,从而保证对象全局唯一
    private Singleton5() {}

    // 静态内部类
    private static class SingleInnerHolder{
        private static Singleton5 instance = new Singleton5();
    }

    // 对外提供获取唯一实例的静态方法
    public static Singleton5 getInstance() {
        return SingleInnerHolder.instance;
    }
}

你肯定觉得这不是饿汉式么,对象直接就创建了,实际这个对象也是延时创建的,因此也可以理解成是懒汉式的一种特殊形式。

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。

这里当Singleton5类被加载时,其静态内部类SingletonHolder没有被主动使用,只有当调用getInstance方法时, 才会装载SingletonHolder类,从而实例化单例对象。 

这样的方式既能实现懒汉式对象的延时创建,也保证了线程的安全。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。
类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化:

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

七、枚举式

public enum Singleton6 {
    // 定义1个枚举的元素,即为该类的唯一实例
    INSTANCE;

    public void anyMethod(){
        System.out.println("任何一个方法!");
    }
}
/**
 * Feng, Ge 2020/2/29 15:34
 */
public class TestEnum {
    public static void main(String[] args) {
        Singleton6 singleton6 = Singleton6.INSTANCE;
        singleton6.anyMethod();
    }
}

枚举在java中与普通类一样,都能拥有字段与方法,防止反序列化生成多个实例,在任何情况下,它都是一个单例,因此线程安全。

八、打破单例模式

 1.克隆(clone)

需要做到平时锁死,但是关键时刻我们能够撬开,克隆就是这么一种良性的单例模式破坏方法,具体做法如下:

public class DoubleLockSingleton implements Cloneable {

    private static volatile DoubleLockSingleton doubleLockSingleton;

    public DoubleLockSingleton() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingleton getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingleton.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingleton();
                }
            }
        }
        return doubleLockSingleton;
    }

    // Override by Velociraptors
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // auto created by Velociraptors
        return super.clone();
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }
}
public class SingletonLauncher {
    public static void main(String[] args) throws CloneNotSupportedException {
        // auto created by Velociraptors
        DoubleLockSingleton doubleLockSingleton = DoubleLockSingleton.getInstance();
        DoubleLockSingleton doubleLockSingleton2 = (DoubleLockSingleton)doubleLockSingleton.clone();
        if(doubleLockSingleton == doubleLockSingleton2) {
            System.out.println("Singleton break failed");
        } else {
            doubleLockSingleton.method();
            doubleLockSingleton2.method();
        }
    }

}
//Hello DoubleLockSingleton!
//Hello DoubleLockSingleton!

2.反射(reflect)

通过java的反射机制,何止是创建一个实例,就连映射整个java类本身的结构都可以:

public class SingletonLauncher {
    public static void main(String[] args) throws Throwable {
        // auto created by Velociraptors
        IdlerSingleton idlerSingleton = IdlerSingleton.getInstance();
        Class<?> clazz = Class.forName("com.singleton.d1213.IdlerSingleton");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        IdlerSingleton idlerSingleton2 = (IdlerSingleton) constructor.newInstance();
        if(idlerSingleton == idlerSingleton2) {
            System.out.println("Singleton break failed!");
        }else {
            System.out.println("Singleton break succeed!");
        }
    }
}
// Singleton break succeed!

3.序列化(serializable)

其与克隆性质有些相似,需要类实现序列化接口,相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

例如下面就有一个序列化的类,并且实现了双重锁单例模式:

public class DoubleLockSingletonSerializable implements Serializable {
    private static final long serialVersionUID = 972132622841L;

    private static volatile DoubleLockSingletonSerializable doubleLockSingleton;

    public DoubleLockSingletonSerializable() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingletonSerializable getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingletonSerializable.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingletonSerializable();
                }
            }
        }
        return doubleLockSingleton;
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }
}
public class SingletonLauncher {

    @SuppressWarnings("resource")
    public static void main(String[] args) throws Throwable {
        // auto created by Velociraptors
        DoubleLockSingletonSerializable doubleLockSingletonSerializable = DoubleLockSingletonSerializable.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
        objectOutputStream.writeObject(doubleLockSingletonSerializable);
        File file = new File("tempFile");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        DoubleLockSingletonSerializable doubleLockSingletonSerializable2 = (DoubleLockSingletonSerializable) objectInputStream.readObject();
        if (doubleLockSingletonSerializable == doubleLockSingletonSerializable2) {
            System.out.println("Singleton break failed!");
        } else {
            System.out.println("Singleton break succeed!");
        }
    }
}

想要阻止序列化破坏单例模式,就只需要声明一个readResolve方法就好了。

public class DoubleLockSingletonSerializable implements Serializable {

    private static final long serialVersionUID = 972132622841L;
    private static volatile DoubleLockSingletonSerializable doubleLockSingleton;

    public DoubleLockSingletonSerializable() {
        // constructed by Velociraptors
    }

    public static DoubleLockSingletonSerializable getInstance() {
        if (doubleLockSingleton == null) {
            synchronized (DoubleLockSingletonSerializable.class) {
                if (doubleLockSingleton == null) {
                    doubleLockSingleton = new DoubleLockSingletonSerializable();
                }
            }
        }
        return doubleLockSingleton;
    }

    public void method() {
        System.out.println("Hello DoubleLockSingleton!");
    }

    private Object readResolve() {
        return doubleLockSingleton;
    }
}
posted @ 2022-06-25 14:02  沙滩de流沙  阅读(675)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货