单例模式

单例模式

在整个运行时域,一个类只有一个实例对象

实现单例模式需要考虑的三点

1.是否线程安全
2.是否懒加载
3.能否反射破坏

单例模式的实现方式

懒汉式

实例对象式第一次被调用才构建,而不是启动时构建好的等你调用
好处:懒加载
缺点:线程不安全。在执行if (instance == null)时可能会有多个线程同时进入,就会实例化多次

public class Singleton {

    //构造器私有,其他类无法用new来构造找个对象的实例
    private Singleton() {}

    //初始化对象为null
    private static Singleton instance = null;

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

懒汉式(线程安全)

好处:懒加载且线程安全,使用synchronized使得同一时刻只有一个线程能够进入getInstance方法
缺点:每次获取对象时都要进行同步操作,影响性能

public class Singleton {

    private Singleton() {}

    private static Singleton instance = null;

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

饿汉式

从上面2个懒汉式对比中,可以看出线程安全问题出现在了构建对象的阶段,只要在编译期构建对象,在运行时调用,就不需要考虑线程安全问题了
好处:线程安全
缺点:不是懒加载,当构建对象开销大时,如果这个对象在项目启动时就构建,万一从来没被调用过,就会浪费资源

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

分析引入

线程安全的懒汉式中,形成低效的原因在于sychronized
那么只要在构建对象时同步,而可以直接使用对象时就没必要同步

public class Singleton {

    private static Singleton instance;

    private Singleton () {}

    public static Singleton getInstance() { //1
        if (instance == null) { //2
            synchronized (Singleton.class) { //3
                instance = new Singleton(); //4
            }
        }
        return instance;
    }
}

现在所有线程都可以直接进入getInstance,然后进入第1步进行判断,如果实例对象还没构建,那么多个线程开始争抢锁,抢到手的那个线程开始创建实例对象,实例对象创建之后,以后所有线程在执行到第2步时,都可以直接跳过,返回实例对象进行调用,这样就不用去争抢锁,解决了低效的问题
但是,在多个线程执行语句2后,虽然只有一个线程抢到锁去执行语句3,但可能会有其他线程已经进入了if代码块,此时正在等待,一旦一个线程执行完,那么这个等待的线程就会立即获取锁,然后进行对象创建,这样对象会被创建多次,这样线程又不是安全的了

双检锁(DCL,double-checked locking)

两次对对象进行判空

public class Singleton {

    private static Singleton instance;

    private Singleton () {}

    public static Singleton getInstance() {
        if (instance == null) { //1
            synchronized (Singleton.class) {
                if (instance == null) { //2
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
}

假设a、b两个线程同时进入getInstance方法
a首先获取锁,b在进行等待,然后进行instance的构建,a构建完成后交还锁
a交还锁后b也会立即获得锁,此时它会在语句2中执行判断,而instance已经被线程a初始化,所以instance不为null,于是线程b将会直接退出,返回实例
这样一看就解决了上面几点问题
但是仍存在问题,这里多线程环境下没有遵循happens-before原则
instance = new Singleton();在指令层面,不是一个原子操作,分为三步
1.分配内存
2.初始化对象
3.对象指向内存地址
在真正执行时,JVM虚拟机为了效率可能会对指令进行重排,比如先执行第1步,再执行第3步,再执行第2步
按此顺序,线程a执行到第3步时,此时instance还没被初始化,假设此时有一个线程b执行到了语句1if (instance == null),此时在线程b中,instance == null返回false,直接跳到return instance;,而线程a中instance对象还未完成初始化,b线程中调用getInstance返回还未被初始化的instance,出现线程不安全的情况。

public class Singleton {

    private volatile static Singleton instance;

    private Singleton () {}

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

从happens-before原则出发,只要给instance加上volatile修饰,就能阻止作用在instance上的指令重排问题
最常用写法,既满足懒加载,也满足线程安全,且性能比较高,就是写起来比较复杂

静态内部类
public class Singleton {

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton(){}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

利用了Jdk类加载机制的特性实现了懒加载,同时也是线程安全的,也兼并了性能

以上几种均能反射破坏

反射破坏测试

public class Main {
    public static void main(String[] args) throws IllegalAccessException, 
            InvocationTargetException, InstantiationException {

        Constructor c = null;
         
        try {
            //构造器
            c = Class.forName("Sington.DCL.Singleton").getDeclaredConstructor();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (c != null) {
            c.setAccessible(true);
            Singleton singleton1 = (Singleton)c.newInstance();
            Singleton singleton2 = (Singleton)c.newInstance();
            System.out.println(singleton1.equals(singleton2));// false,不是一个对象,非单例
        }
    }
}

反射从运行时类型信息中获取了构造器,并通过构造器构造了对象,而单例的目的就是阻止外部来构建对象
目前问题是如何拒绝JVM来读取类的私有方法,这时应该就无法通过上层代码来实现了

枚举

但是Jvm本身也提供了这种机制,就是枚举类型,对于枚举类型反射是无法获得它的构造器的,因此反射就不能破坏枚举类型的单例,而且枚举类型本身也能够保证线程安全,但是枚举无法实现懒加载,它在程序启动之初,就已经把这个内部的实例,完全构建好来提供给使用者

public enum  Singleton {
    INSTANCE;
}
由于反射破坏是通过人为方式的,所以一般不考虑,常采用的为DCL双检锁静态内部类的写法
posted @ 2020-08-04 20:09  飞天小海星  阅读(122)  评论(0)    收藏  举报