详解Java单例模式是如何实现并保证安全的?

详解Java单例模式是如何实现并保证安全的?,第1张

详解Java单例模式是如何实现并保证安全的?

单例模式主要分为饿汉式和懒汉式,下边是Java对单例模式的实现,以及一些细节问题,都是面试高频考点

饿汉式单例

饿汉式在类被加载时就创建了单例对象,需要时调用静态方法getInstance()来获取该单例对象

public class 饿汉式 implements Serializable {
    private 饿汉式() {
        // 防止反射获取构造器创建对象
        if(INSTANCE != null){
            throw new RuntimeException("单例对象只能被创建一次!");
        }
        System.out.println("私有构造方法保证类不被创建");
    }
    public static final 饿汉式 INSTANCE = new 饿汉式();

    public static 饿汉式 getInstance(){
        return INSTANCE;
    }

    public static void otherMethod(){
        System.out.println("当直接调用其他方法时,会出发类的构造,使得内部单例对象被创建");
        System.out.println(INSTANCE);
    }

    /**
     * 防止反序列化来构造新的实例,反序列化时会使用该方法的返回值作为结果,而不是重写
     */
    public Object readResolve(){
        return INSTANCE;
    }
}

破坏单例的方法:

  • 反射,通过反射获得该类的构造器,执行构造器造出新的对象
  • 反序列化,先把单例对象序列化,再把数据反序列化就获得了新的对象,静态的单例对象不会被序列化回来,就会导致造出新的数据
  • unsafe类直接创建新的对象

解决措施:

  • 对于反射,只需要在私有构造器中判断当前单例是否已经被创建,创建就抛出错误,即可
  • 反序列化,可以实现readResolve()方法,其中返回单例对象,当对象被反序列化时,就会查看该类是否重写该方法,这里重写了就直接返回。(原理:反序列化会先生成一个代表该数据的对象,然后通过反射获得类对象看看是否重写了readResolve()方法,发现重写了,就把指针指向该方法的返回值,之前创建的对象设置为null,等待GC)
  • unsafe无法解决
枚举饿汉式

枚举类就是使用单例模式来实现的,只需要在枚举类中只写一个枚举,他就是单例的,利用枚举的内部实现,就可以实现单例模式

懒汉式

类被加载时并不会创建单例对象,当第一次getInstance()才会创建单例对象

非线程安全实现

public class 懒汉式 {
    private 懒汉式(){
        System.out.println("类加载!");
    }
    private static 懒汉式 singleton = null;

    public 懒汉式 getInstance(){
        if(singleton == null){
            singleton = new 懒汉式();
        }
        return singleton;
    }
}

为什么会出现线程安全问题:
当两个线程同时走到 **if(singleton == null){**时,就会都判断通过,同时创建新对象,造成线程安全问题
简单的解决办法就是在方法上加synchroinzed关键字锁住类对象,但是并发度很低,而且成功创建对象之后,其实再进入该方法并不会产生修改,是线程安全的,但仍然存在并发问题

DCL懒汉式

提高多线程懒汉式的并发度

public class 懒汉式 {
    private 懒汉式(){
        System.out.println("类加载!");
    }
    private static volatile 懒汉式 singleton = null;

    public 懒汉式 getInstance(){
        if(singleton == null){
           synchronized (懒汉式.class){
               if(singleton == null){
                   singleton = new 懒汉式();
               }
           }
        }
        return singleton;
    }
}

分析:
并不在方法上加锁,因为并发度很低,先判断是否为null,如果不是null,直接返回,这里多线程都不会出现问题,如果需要创建单例,才会进入锁逻辑来创建单例
为什么锁里边还得判断null:
因为可能两个线程排队进入锁,一个创建单例后,退出锁,另一个线程进入锁,发现已经创建就会直接退出锁,这就完全保证了多线程下懒汉式的线程安全!(并不是 还有volatile 的帮助!!!)

双检锁:
外层检测是为了提高并发度,不加的话不管单例是否已经创建,线程都会排队执行,实际上是不需要这样的。
内层检测是为了安全,因为可能同时有两个线程排队进入锁并想创建新的对象

那单例对象为什么要加volatile 修饰呢:
首先volatile作用是保证变量的可见性和有序性,
这里主要作用是保证有序性。
通过反编译后
可见new一个单例时有四条字节码指令

new  								分配对象空间
dup									不知道
invokespecial					执行构造方法
putstatic 							为类的静态变量赋值

CPU可能会对指令进行优化,修改执行顺序(前提是从结果角度来看不会产生影响的修改,有些影响是人为才能看出来的),new 肯定再 构造之前,但静态变量赋值工作和构造方法对成员变量赋值两步并没有先后顺序,所有可能会出现重排,
当一个线程进入锁,创建单例时,指令发生重排,先赋值了静态变量,这时候新线程来第一次check发现静态变量不是null,就直接返回了,但是这时候的单例对象的构造方法并没有开始执行,这就产生了问题,新线程返回了一没有经过初始化的对象

内部类懒汉式

JVM在类加载时对静态变量的赋值是线程安全的,JVM自己保证的,所以将实例对象放入静态内部类中,既可以保证在调用时才会创建(懒汉),又可以保证线程安全!

public class 懒汉内部类 {
    private 懒汉内部类(){
        System.out.println("懒汉式私有构造器");
    }
    private static class Holder{
        static 懒汉内部类 INSTANCE = new 懒汉内部类();
    }
    public static 懒汉内部类 getInstance(){
        return Holder.INSTANCE;
    }
}

JDK那些地方体现了单例模式?
  • Runtime类
  • System类中的Console类
  • Collections的一些内部类等等

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/735834.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-27
下一篇 2022-04-27

发表评论

登录后才能评论

评论列表(0条)

保存