津津乐道设计模式 - 单例模式详解

1、什么是单列模式

单例模式(Singleton)指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。举个例子:屏幕前的你幻想一下现在你有5个漂亮的老婆,他们的老公都是你,那么你就是家里的Singleton,5个老婆喊“老公”(全局访问点),都是指向一个人那就是你。这样理解清楚了吧?清楚了那么赶紧醒醒回到现实,不要想老婆的事了,跟着老师继续学习,女人只会影响我们撸码的速度~

在J2EE标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

2、饿汉单例模式

饿汉单例模式在类加载的时候就立即初始化,并且创建静态单例对象供系统使用,我们常说饿汉单例模式是线程安全的,因为在线程还没出现它就已经实例化了,不会存在访问安全问题,直接用于多线程也不会出现问题。我知道又有人要说了,哎呀你说这些理论东西,我也理解不来,那么还是一个的例子:现在要进行产品的销售,加载类时,先把商品(实例)准备好,现在你需要,我直接给你。

//常规写法
public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }
}

还有一种可以利用代码块机制的写法

//利用静态代码块的写法
public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungryStaticSingleton;
    static {
        hungryStaticSingleton = new HungryStaticSingleton();
    }
    private HungryStaticSingleton() {
    }
    public static HungryStaticSingleton getInstance() {
        return hungryStaticSingleton;
    }
}

测试是不是唯一实例,可以调用getInstance的hashCode方法验证结果是不是一致

//测试是否唯一 不会新new一个实例
public static void main(String[] args) {
System.out.println(HungryStaticSingleton.getInstance().hashCode());
System.out.println(HungryStaticSingleton.getInstance().hashCode());
}

从以上描述我们可以看出饿汉单例模式的优点:线程安全,执行效率高;但是同样因为类加载的时候就已经初始化,不管你用不用它都在内存占用着,形象说就是“占着茅坑不拉屎”,这个也是它的缺点。

3、懒汉单例模式

懒汉单例模式:类加载时没有生成单例,当外部类调用的时候内部类才会加载(getlnstance)去创建这个单例。以代码展现简单实现:

public class LazySingleton {
    private LazySingleton(){}
    private static LazySingleton lazy = null;
    public static LazySingleton getInstance(){
        if(lazy==null){
            lazy = new LazySingleton();
        }
        return lazy;
    }
}

上面的代码在单线程中不会出现问题,如果在多线程应用中,线程是不安全的,还是销售产品来说,现在产品不是按照饿汉单例模式了,是你预定了我才给你,那么此时有两个顾客A(线程A)和顾客B(线程B),这个时候顾客A预定了,调用getInstance()方法,假设程序还没走完,顾客B又预定了,原本按照代码是只要lazy 存在是不会再new LazySingleton()的,但是因为顾客A调用的getInstance()还没执行完成,lazy==null,而顾客B又预定了,即可以理解是new 了两次LazySingleton,破坏了破坏了单例。

如何解决?就是在getInstance()加synchronized关键字,从而使得线程同步

public class LazySyncSingleton {
	private static LazySyncSingleton lazy = null;
    private LazySyncSingleton(){}
    public synchronized static LazySyncSingleton getInstance(){
        if(lazy==null){
            lazy = new LazySyncSingleton();
        }
        return lazy;
    }
}

上述代码可以解决线程安全问题,也就是顾客A预定了,就必须等顾客A拿到产品,顾客B才可以预定,因为synchronized是线程阻塞的,在线程数量较多的情况下,会导致大量线程阻塞,解决这个方式也很简单,双重检查的方式,也即是A顾客预定产品,在没拿到产品前,B顾客不需要等待已久可以预定,因为synchronized是在getInstance()内部阻塞,只要getInstance()内部逻辑不复杂,在顾客A拿到产品的同时,顾客B几乎无感知也拿到产品了。

public class LazyDoubleCheckSingleton {
    
    private volatile static LazyDoubleCheckSingleton lazy = null;
    
    private  LazyDoubleCheckSingleton(){}
    //双重检查
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazy==null){
            synchronized(LazyDoubleCheckSingleton.class){
                if(lazy==null) {
                    lazy = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazy;
    }
}

以上方式加了synchronized关键字,无论如何都会加锁,对程序而言性能依旧存在影响的,那么是否有完美的解决方案?答案是有的,不知道大家想到饿汉模式的静态内部类方式了么?懒汉模式搭配静态内部类方式可以完美解决

//最优的单例模式写法
public class LazyStaticSingleton {
    private LazyStaticSingleton(){
        if(lazyHolder.LAZY!=null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }
    private static final LazyStaticSingleton getInstance(){
        //return之前会先加载内部类lazyHolder
        return lazyHolder.LAZY;
    }
    //默认不会加载
    private static class lazyHolder{
        private static final LazyStaticSingleton LAZY = new LazyStaticSingleton();
    }
}

以上写法即满足了饿汉模式内存占用“占着茅坑不拉屎”,也解决了synchronized性能的问题。

4、注册式单例模式

注册式单例模式意思是将每个实例都注册登记起来,给一个唯一标示来获取,还是产品销售的例子,顾客A预定产品,这个产品就标记起来(A货架),顾客B预定产品如果是相同产品就按照标示(A货架)直接返回产品,即实例。
注册式单例模式有两种:枚举单例模式 + 容器单例模式

//枚举式单例
public enum EnumSingleton {

    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

//容器式单例模式
public class ContainerSingleton {
    private ContainerSingleton() {}
    private static final Map<String, Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

枚举式单例模式:推荐单例模式的实现写法;
容器式单例模式:使用于实例非常多的情况,便于管理。(Spring中也有应用到该模式,查阅抽象类 AbstractAutowireCapableBeanFactory)
在这里插入图片描述
具体大家可以搜索Spring源码查阅。

5、总结

本文主要介绍了设计模式的单例模式,例举了多种单例模式的实现方式,在日常开发中博主推荐大家使用枚举式单例模式,通过本章节希望对大家对单例模式有一个更清晰的理解;

样例代码:https://github.com/lhmyy521125/toher-designmode

下期预告:津津乐道设计模式 - 代理模式详解

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micro麦可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值