是单例模式!不是单身模式!

本文深入探讨了Java中的单例模式,从饿汉模式开始,逐步讲解懒汉模式、双重检查锁定(DCL)懒汉模式以及枚举单例模式。讨论了各种模式的实现细节、优缺点以及线程安全性,揭示了单例模式在多线程环境中的挑战和解决方案。
摘要由CSDN通过智能技术生成

在这里插入图片描述

中午,我和弟弟说:今天看了一种设计模式,感觉和你很搭配!

写代码的弟弟斜眼道:what?

我呵呵一笑说:单身模式啊

弟弟一副惊讶的样子,问单身什么鬼!

我说:单身模式追求永远一个人,在这种模式下,构造器是私有的,实例只能在对象内部创建。

弟弟:这是单例模式,不是单身模式。

我:没错单身模式又有很多种实现方法,经典的有饿汉单身模式、懒汉单身模式、DCL懒汉单身模式(双重检测锁)、枚举单身模式。

弟弟:是单例模式,不是单身模式!饿汉单例模式我知道,在类中使用new创建一个私有的静态的实例对象,然后使用公有静态方法getInstance()让外部能获取这个类。

/**
 * 饿汉模式
 *
 * @author czy
 * @date 2021/6/11
 */
public class Hungry {
   private Hungry(){
       System.out.println("饿汉模式");
   }
   private static Hungry HUNGRY = new Hungry();

   public static Hungry getInstance(){
       return HUNGRY;
   }
}

我:哎呦!不错哦,你知道的太多了。但是饿汉单身模式饥不择食,加载类的时候就会初始化里面的单例对象,也不看看现在地皮有多贵!

弟弟:再说一遍,是单例模式!所以有了升级版,懒汉单例模式,这种模式下,只创建了单例对象的引用,没有赋值,当有人第一次调用getInstance()获得单例对象的时候才赋值,这样不用就不触发的理念明显节约了不必要的开支。

public class LazyMan {
    private LazyMan(){
        System.out.println("懒汉单例模式");
    }
    private static LazyMan lazyMan;

    public static LazyMan getInstance(){
        if (lazyMan==null){
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

我:单…

弟弟:你别说话,这样虽然节省了空间,但是在首次有人需要使用单例对象的同时,有多人访问getInstence()获取对象接口,这时大家判断单例对象都是null,于是这些人会分别新建实例,这样就会破坏实例的单一性。

我:加锁啊!基操

弟弟:加做有很多种,可以直接加在获取实例的方法上

public class DCLLzayMan {    
	private DCLLzayMan(){       
			System.out.println("双重检测锁的懒汉模式");
	}    
	private  static DCLLzayMan dclLzayMan ;   
	//这种方式太消耗资源。每次调用都要加锁
	public static synchronized DCLLzayMan getInstance(){
		if (dclLzayMan==null){
			dclLzayMan = new DCLLzayMan();
		}
		return dclLzayMan;
	}    
}

我:不是良配,开销太大,还不如饿汉模式

弟弟:可以缩小锁的范围,在判断实例为空后,对单例类进行加锁,为了防止多人都走到抢锁环节,需要再判断是否为空,如果为空就new出实例。

public class DCLLzayMan {
    private DCLLzayMan(){
        System.out.println("双重检测锁的懒汉模式");
    }
    private  static DCLLzayMan dclLzayMan ;
      //测试指令从排序,发生概率很小,不要轻易尝试
    public static DCLLzayMan getInstance(){
        if (dclLzayMan==null){
            synchronized (DCLLzayMan.class){
                if (dclLzayMan==null){
                    //不是原子性操作
                    dclLzayMan = new DCLLzayMan();
                }
            }
        }
        return dclLzayMan;
    }
}

我:现在安全了?你确定?

弟弟:单线程当然是安全的,但是现在流行多线程,线程多了,就会出幺蛾子!

我们知道new Object()操作不是原子性的,共分三步:

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

不巧的是,计算机优化代码时,有种操作叫指令重排,上面操作就变成了:

  1. 为对象分配内存空间
  2. 将内存空间的地址赋值给对应的引
  3. 初始化对象

我们在2执行之后,对象只用内存空间,空间内却没有值。这时候其他人过来获取实例,一看实例已经分配空间了,就是不等于null,就会直接把这个没有初始化的残缺对象拿走使用。严重缺乏安全感!为了解决这个问题,我们请来了打佬volatile,防止创建对象时指令重排序。

public class DCLLzayMan {
    private DCLLzayMan(){
        System.out.println("双重检测锁的懒汉模式");
    }
    
    //volatile:我来禁止指令重排序
    private volatile static DCLLzayMan dclLzayMan ;
    //测试指令从排序
    public static DCLLzayMan getInstance(){
        if (dclLzayMan==null){
            synchronized (DCLLzayMan.class){
                if (dclLzayMan==null){
                    //不是原子性操作
                    dclLzayMan = new DCLLzayMan();
                }
            }
        }
        return dclLzayMan;
    }
}

我:你以为这就安全了?

弟弟:傻瓜,这又不是枚举类怎会安全!我们通过反射技术很容易对单例私有属性关闭校验。就可以为所欲为了,但是枚举类在这方面表象的坚定不移,不会因此被强行破坏单例的单一属性。

给你看看枚举类单例

**
 * 无法被反射修改,因此是安全的
 *
 * @author czy
 * @date 2021/6/11
 */
public enum EnumSingle {
    SINGLE;

    public static EnumSingle getInstance(){
        return SINGLE;
    }
//送你一个测试方法
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(EnumSingle.getInstance().hashCode());
            }).start();
        }
    }
}

还有,记住了!是单例模式,不是单身模式!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值