最近学习了一下单例模式, 看 bilibili up 主 "狂神说 Java" 讲完后, 发现大部分博客都少了一个很有趣的环节, 不分享出来实在是太可惜了, 原视频 https://www.bilibili.com/video/BV1K54y197iS
1, 了解单例
这个部分小部分我相信很多博客都讲的很好, 我就尽量精简了
注意:
单例类只能有一个实例
这个实例由自己创建
这个实例必须提供给外界
关键: 构造器私有化
创建方法:
饿汉式
懒汉式
总结: 我认为创建方法可以归根于两种, 一种是饿汉式, 我在类的加载的时候就创建; 还有一种懒汉式, 只有在我需要的时候才去创建
2, 思路及实现
[饿汉模式最基本的实现]
在类加载的时候就已经创建了, 这个模式下, 线程是安全的, 不同的线程拿到的都是同一个实例, 但是, 这个也存在空间浪费的问题, 我不需要的时候你也加载了.
- // 饿汉模式
- public class HungerSingle {
- private static HungerSingle single = new HungerSingle();
- // 构造器私有, 外界不能通过构造方法 new 对象, 保证唯一
- private HungerSingle() {
- }
- // 提供外界获得该单例的方法, 注意方法只能是 static 方法, 因为没有类实例
- public static HungerSingle getInstance(){
- return single;
- }
- }
[懒汉模式最基本的实现]
为了解决上述那个空间浪费问题, 这时候懒汉模式就起作用了, 你需要我的时候我再去创建这个实例
- // 懒汉模式
- public class LazySingle {
- private static LazySingle single;
- // 构造器私有化, 禁止外部 new 生成对象
- private LazySingle(){
- }
- // 外界获得该单例的方法
- public static LazySingle getInstance(){
- if(single == null){
- single = new LazySingle();
- }
- return single;
- }
- }
一位热心前辈的评论:"像你这样写单例, 在我们公司是要被开除的."
趁我还是学生, 怀着以后不被开除的心情, 继续学习下去
原来懒汉模式下, 单例线程是不安全的.
怎么测试呢? 如下
[测试懒汉模式线程不安全]
- //1, 构造器
- private LazySingle(){
- System.out.println(Thread.currentThread().getName());
- }
- // 创建十个线程
- for (int i = 0; i <10; i++) {
- new Thread(()->{
- Singleton2.getInstance();
- }).start();
- }
此时你会发现, 构造方法调用了不止一次, 说明没有实现预期的单例
平时我们解决线程不安全的方法: 不就是线程不安全嘛, 那好办, 加锁
[双重检测锁 / DCL]
- public class DCLSingle {
- private static DCLSingle single;
- private DCLSingle(){
- }
- public static DCLSingle getInstance(){
- // 第一次判断, 没有这个对象才加锁
- if(single == null){
- // 哪个需要保护, 就锁哪个
- synchronized (DCLSingle.class){
- // 第二次判断, 没有就实例化
- if(single == null){
- single = new DCLSingle();
- }
- }
- }
- return single;
- }
- }
仔细和别人代码一比对, 发现我少了个 volatile 关键字, 这是啥玩意?
不懂就问.
[volatile]
为了避免指令重排
- // 上述代码声明上面加上 volatile 关键字
- private volatile static DCLSingle single;
啥是 volatile ?
引用自别人博客
https://www.cnblogs.com/YLsY/p/11295732.html
加 volatile 是为了出现脏读的出现, 保证操作的原子性
1, 原子性操作: 不可再分割的操作
例如: single = new DCLSingle();
其实就是两步操作:
1new DCLSingle();// 开辟堆内存
2singl 指向对内存
2, 脏读
Java 内存模型规定所有的变量都是存在主存当中, 每个线程都有自己的工作内存.
线程对变量的所有操作都必须在工作内存中进行, 而不能直接对主存进行操作.
并且每个线程不能访问其他线程的工作内存.
变量的值何时从线程的工作内存写回主存, 无法确定.
3, 指令重排
single = new DCLSingle();
先执行2
后执行1
// 先指向堆内存, 还未完成构造
[模拟情况]
1线程 1 执行, 在自己的工作内存定义引用, 先指向堆内存, 还未构造完成
2此时线程 2 执行, 它进行判断, 引用已经指向了内存, 所以线程 2, 认为构造完成, 实际还未构造完成
还有一种差点忘记说了, 也是菜鸟教程说建议使用的方式
[静态内部类实现单例]
- public class Singleton {
- private Singleton(){}
- private static class SingleIN{
- private static final Singleton INSTANCE = new Singleton();
- }
- private Singleton getInstance(){
- return SingleIN.INSTANCE;
- }
- }
你会发现它和前面讲的普通饿汉式很像, 我把它也归于饿汉式一类, 因为它也是直接就 new Singleton, 但是它却有着懒加载的效果, 而这种方式是 Singleton 类被装载了, instance 不一定被初始化. 因为 SingletonHolder 类没有被主动使用, 只有通过显式调用 getInstance 方法时, 才会显式装载 SingletonHolder 类, 从而实例化 instance.
[建议] 建议使用静态内部类实现
## 3, 如何破化单例 (其它大部分博客没有的内容) 在这里感谢 b 站 up[狂神说 java]
在面试官面前装逼的时候来了
java 语言实现动态化的灵魂 -- 反射, 说: 没有什么是我不能改变的, 看我来如何操作.
[反射破坏单例]
- public class DCLSingle {
- private static DCLSingle single;
- private DCLSingle(){
- }
- public static DCLSingle getInstance(){
- // 第一次判断, 没有这个对象才加锁
- if(single == null){
- // 哪个需要保护, 就锁哪个
- synchronized (DCLSingle.class){
- // 第二次判断, 没有就实例化
- if(single == null){
- single = new DCLSingle();
- }
- }
- }
- return single;
- }
- // 通过反射破化单例
- public static void main(String[] args) throws Exception {
- LazySingle single = LazySingle.getInstance();
- Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
- constructor.setAccessible(true);
- LazySingle single1 = constructor.newInstance();
- System.out.println(single == single1);//false
- }
- }
得到单例类的构造器, 然后通过 newInstance 的方法创建对象, 很明显破化了单例
[改进代码, 防止你搞破化]
既然这次你是通过得到构造器破化的, 那我给构造器加个方法, 如果你已经创建了实例, 那就抛出异常
- private LazySingle(){
- synchronized(LazySingle.class){
- if(single!=null){
- throw new RuntimeException("破坏失败");
- }
- }
- }
但是这个又有问题, 这里的判断是 private static DCLSingle single 是否有值, 如果我们都不通过 getInstance() 方法创建对象, 而是这样
- public static void main(String[] args) throws Exception {
- // LazySingle single = LazySingle.getInstance();
- Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
- constructor.setAccessible(true);
- // 注意: 这里的对象不是单例类中里面属性的那个对象
- LazySingle single = constructor.newInstance();
- LazySingle single1 = constructor.newInstance();
- System.out.println(single == single1);//false
- }
这里根本不会抛出异常, 而是又破坏了单例
[继续改进代码, 防止搞破化]
简直就是相爱相杀呀, 我们可以利用红路灯原理, 防止破化
改进构造方法
- // 加个标志
- private static String sign = "password";
- private LazySingle(){
- synchronized(LazySingle.class){
- if(single!=null || !"password".equals(sign)){
- throw new RuntimeException("破坏失败");
- }else{
- sign = "no";
- }
- }
- }
此刻你通过上述 main() 方法里面的内容测试, 发现又会抛出异常. 然而我们能通过反射获得构造方法, 那我们同样也能通过反射获取对象的属性以及值吧
[再度破化]
- public static void main(String[] args) throws Exception {
- Constructor<LazySingle> constructor = LazySingle.class.getDeclaredConstructor();
- constructor.setAccessible(true);
- Field field = LazySingle.class.getDeclaredField("sign");
- // 此处省略通过反射获取该属性的类型和方法....
- LazySingle single1 = constructor.newInstance();
- // 重新变回原标志位
- field.set("sign","password");
- LazySingle single2 = constructor.newInstance();
- System.out.println(single2 == single1);//false
- }
又被破化了
[再次改进]
我们将目光抛向枚举,
jdk1.5 之后, 出现枚举
利用枚举实现不仅能避免多线程同步问题, 而且还自动支持序列化机制, 防止反序列化重新创建新的对象, 绝对防止多次实例化 (菜鸟教程官方术语)
- public enum Singleton {
- INSTANCE;
- public Singleton getInstance() {
- return INSTANCE
- }
- }
[反射能破化枚举的单例吗?]
我们先要了解枚举是啥, 它的底层是怎么实现的
我们会发现枚举本身就是一个
通过反编译工具, 查看枚举底层的构造方法
通过反射获取构造方
重复上述测试
我们最终可以发现反射不能破化枚举的单例
这种实现方式还没有被广泛采用, 但这是实现单例模式的最佳方法. 它更简洁, 自动支持序列化机制, 绝对防止多次实例化.(菜鸟教程官方)
[总结] 太难了
来源: https://www.cnblogs.com/yxm2020/p/12723418.html