线程安全的单例模式

本文探讨一下线程安全下单单例模式, 参考了多篇文章, 结合自己的一些经验.

饿汉模式

饿汉模式本身是一种提前创建好实例的方式, 利用jvm 在加载类的时候, 只会加载一次, 以及加载类本身jvm是通过加锁保证线程安全的, 因此饿汉模式将实例作为类的静态变量, 就实现了线程安全的单例.

写法

public class Singleton {

    // 静态变量 在类加载后就实例化完成
    private static Singleton instance = new Singleton();
   
    // 构造方法私有化 防止直接通过类创建实例
    private Singleton() {
    }
   
    public static Singleton getInstance() {
        return instance;
    }

}
复制代码

缺点

  • (1) 类在加载时就创建好了实例变量, 啥时候用就不确定了, 存在浪费资源的可能性
  • (2) 无法解决序列化后, 再次反序列时, 无法保证单例了
  • (3) 无法避免反射的攻击

懒汉

懒汉模式就是说再第一次需要单例对象的时候, 再完成单例对象的生成, 最优写法就是利用synchronized关键字 + volatile实现双重校验.

写法

public class Singleton {


    private static volatile Singleton instance;
    
    // 构造方法私有化 防止直接通过类创建实例
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            // 第一次使用, 保证线程安全
            synchronized (Singleton.class) {
                // 并发情况下 可能有多个线程阻塞在上面一行
                // 某个线程完成创建后, 多个线程会陆续进入到下面一行, 为了避免重复生成
                // 需要二次校验
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        
        // 如果已经创建好 直接返回
        return instance;
    }
}
复制代码

缺点

(1) 依旧是反序列化的问题, 如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

(2) 无法避免反射的攻击

静态内部类

在单例类中创建一个静态内部类, 这样单例类在被创建的时候, 静态内部类不会完成类的加载, 只有当真正使用到静态内部类的时候, 才会去创建相应的类对象, 完成对单例对象的加载, 实现了懒加载.

Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显式装载SingletonHolder类

写法

public class Singleton {

    // 构造方法私有化 防止直接通过类创建实例
    private Singleton() {
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
    
    private static class SingletonHolder {
        
        // 在静态内部类中完成实例的创建
        // 即当真正使用到SingletonHolder这个类的时候, 才会去加载SingletonHolder类对象
        // 那么自然这个时候才去创建相应的静态变量, 实现了延迟加载的功能
        private static final Singleton instance = new Singleton();
        
    }

}
复制代码

缺点

(1) 依旧存在反序列化的问题.

(2) 无法避免反射的攻击

枚举

枚举实现单例, 有两种方式,

方式1

这种方式是将枚举作为内部类来使用, 毕竟枚举也是一个类, 因此枚举类也具有延迟加载的能力, 不同的是, 枚举类是通过jvm会保证枚举类的构造函数只会执行一次这个特点, 来保证单例的.

写法

public class Singleton {

    // 构造方法私有化 防止直接通过类创建实例
    private Singleton() {
    }

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

    
    // 枚举类
    private enum SinletonHolder {
        
        INSTANCE;
        
        private Singleton instance;

        // jvm会保证枚举类的构造函数只被执行一次
        SinletonHolder() {
            instance = new Singleton();
        }

        // 枚举类里的方法需要通过枚举值进行调用
        private Singleton getInstance() {
            return instance;
        }
        
    }
}
复制代码

方式2

第二种方式, 就是直接将枚举类认为是我们的单例的类,

写法

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        // 你想要通过单例做什么, 就可以在这里面实现
    }
    
}
复制代码

缺点

首先强调下, 这两种写法功能性上没什么差别, 都是通过枚举类的特点来保证单例和线程安全

(1) 没啥缺点, 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊.

(2) 也解决了反射攻击的问题

序列化问题

为啥普通方式单例对象反序列化时会创建新对象?

任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”

当然,这个问题也是可以解决的,可以通过对序列化机制进行定制, 主要就是readResolve方法

枚举类为啥没有反序列化的问题?

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。

同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

反射问题

普通类方式实现枚举为啥有反射问题?

根据java的反射, 获取到类的class对象后, 可以利用它的构造函数创建新的实例, 即使它的构造函数是私有的, 依然可以调用

普通单例模式如何解决反射问题?

可以在私有的构造函数中判断一下属性是不是null, 如果不是null, 就抛出异常.

public class Singleton {

    private static Singleton instance;
    private Singleton() {
        if (instance != null) {
            // 这样就避免了反射攻击的问题
            throw new RuntimeException();
        }
    }

}
复制代码

枚举为啥没有反射问题?

反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

利用CAS实现单例模式

如果我不想使用synchronized关键字来实现单例模式呢? 即使是类加载保证的线程安全, 底层也是加了锁的, 那么就可以采用CAS实现.

CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

写法

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            // CAS操作
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}
复制代码

缺点

(1) 如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享