【设计模式达摩院】01 单例模式之“回字的八种写法”

引言

我用这个标题告诉那些没有接触单例模式的同学,单例有8中写法只多,不要小看单例模式。同时告诉那些了解单例模式的人,我们不能学孔乙己,咬文嚼字敲砖弄沙认为懂了8中模式就是懂了单例。

单例模式是什么

单例模式对于一个类只能创建一个实例,即在JVM中只有一块内存代表该对象的实例。

  1. 确保任何时候使用该实例的时候所有的资源都是相同的。
  2. 单例的类有义务提供全局访问的入口即getInstance。

为什么要有单例模式

  1. 资源节约,避免重复的对象占用宝贵的内存资源
  2. 全局共享,如数据库连接,线程池等。就像你最好对外提供一个证件,太多的切换不是容易控制的。

单例的8中写法

01 :基本功

/**
 * 唯一缺点:类加载的时候就会创建实例
 *
 * 注:工作中常用的反而是这种略带缺陷的方法,简单实用
 */
public class Mgr01 {

    //JVM保证每个CLASS只会LOAD到内存一次
    private static  final Mgr01 instance = new Mgr01();

    //私有构造器
    private Mgr01() {
    }

    public static Mgr01 getInstance() {
        return instance;
    }


}

02 同上

/**
 * 同Mgr01本质一样
 */
public class Mgr02 {

    //JVM保证每个CLASS只会LOAD到内存一次
    private static  final Mgr02 INSTANCE;
    static {
        INSTANCE = new Mgr02();
    }
    //私有构造器
    private Mgr02() {
    }

    public static Mgr02 getInstance() {
        return INSTANCE;
    }


}

03 饿汉式

饿汉仅仅是个花名,如果直接创建对象,而该对象特别消耗资源,可以考虑下面模式。
注:实际上如果你可以接收01的写法,通常就很完美

package com.kouryoushine.deginmodel.singleton.msb;

/**
 * @ClassName Mgr01
 * @Description TODO
 * @Author kouryoushine
 * @Date 2021/3/15 22:36
 * @Version 1.0
 */

/**
 * 缺点:多线程访问的时候有影响,不安全
 */
public class Mgr03 {

    //JVM保证每个CLASS只会LOAD到内存一次
    private static Mgr03 INSTANCE;

    //私有构造器
    private Mgr03() {
    }

    /**
     * 懒汉式,获取实例的时候在初始化实例
     * @return
     */
    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            //模拟线程停顿,增加线程切换,暴露多线程问题
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    /**
     * 如果获取到hashcode一致说明对象是一个对象
     * 这里验证:多线程环境可能创建两个不同实例
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr03 instance = Mgr03.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }
    }
}

04 加锁保平安

你说多线程不安全,那好我加锁确保安全。

对一个方法加锁会降低该方法100倍的性能,因此加锁的方法不能再高并发的复杂场景中使用。

package com.kouryoushine.deginmodel.singleton.msb;

/**
 * @ClassName Mgr01
 * @Description TODO
 * @Author kouryoushine
 * @Date 2021/3/15 22:36
 * @Version 1.0
 */

/**
 * 缺点:加锁可能影响效率
 */
public class Mgr04 {

    //JVM保证每个CLASS只会LOAD到内存一次
    private static Mgr04 INSTANCE;

    //私有构造器
    private Mgr04() {
    }

    /**
     * 懒汉式,获取实例的时候在初始化实例
     * @return
     */
    public static synchronized Mgr04   getInstance() {
        if (INSTANCE == null) {
            //模拟线程停顿,增加线程切换,暴露多线程问题
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    /**
     * 如果获取到hashcode一致说明对象是一个对象
     * 这里验证:多线程环境可能创建两个不同实例
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr04 instance = Mgr04.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }

    }
}

05 简单的妄想

下面方法是错误的,等同于没加锁。这种方法很容易想到,深入考虑就发现在一步的时候线程已经不安全了

package com.kouryoushine.deginmodel.singleton.msb;

/**
 * @ClassName Mgr01
 * @Description TODO
 * @Author kouryoushine
 * @Date 2021/3/15 22:36
 * @Version 1.0
 */

/**
 * 缺点:加锁可能影响效率,这里缩小锁的范围优化性能,实际上没有效果,getInstace的第一句就无法保证线程安全
 * 这种方式看似明显的错误,实际上却是直观的解决线程不安全的方法
 */
public class Mgr05 {

    //JVM保证每个CLASS只会LOAD到内存一次
    private static Mgr05 INSTANCE;

    //私有构造器
    private Mgr05() {
    }

    /**
     * 懒汉式,获取实例的时候在初始化实例
     * @return
     */
    public static  Mgr05 getInstance() {
        if (INSTANCE == null) {
            //模拟线程停顿,增加线程切换,暴露多线程问题
            synchronized (Mgr05.class){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }

        }
        return INSTANCE;
    }

    /**
     * 如果获取到hashcode一致说明对象是一个对象
     * 这里验证:多线程环境可能创建两个不同实例
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr05 instance = Mgr05.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }

    }
}

06 双重检查机制

注意双重检查的巧妙之处,在确定之后多一步判断,确保只生产一个实例

package com.kouryoushine.deginmodel.singleton.msb;

/**
 * @ClassName Mgr01
 * @Description TODO
 * @Author kouryoushine
 * @Date 2021/3/15 22:36
 * @Version 1.0
 */

/**
 * 缺点:针对加锁的优化,双重检查机制
 */
public class Mgr06 {

    //JVM保证每个CLASS只会LOAD到内存一次
    // volatile 防止指令重排
    private static volatile Mgr06 INSTANCE;

    //私有构造器
    private Mgr06() {
    }

    /**
     * 懒汉式,获取实例的时候在初始化实例
     * @return
     */
    public static Mgr06 getInstance() {

        if (INSTANCE == null) {
            //模拟线程停顿,增加线程切换,暴露多线程问题
            synchronized (Mgr06.class){
                //双重检查:获取到锁后再次检查是否已经存在实例
                //锁定过程中instance值不会被改变,所以此时判断是准确的
                if(INSTANCE== null){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }

            }

        }
        return INSTANCE;
    }

    /**
     * 如果获取到hashcode一致说明对象是一个对象
     * 这里验证:多线程环境可能创建两个不同实例
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr06 instance = Mgr06.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }

    }
}

07 看起来很美

推荐内部类的写法,简单就是美。

package com.kouryoushine.deginmodel.singleton.msb;

/**
 * @ClassName Mgr01
 * @Description 完美的单例写法之一
 * @Author kouryoushine
 * @Date 2021/3/15 22:36
 * @Version 1.0
 */

/**
 * 静态内部类的方式实现
 * 1. JVM保证每个CLASS只会LOAD到内存一次
 * 2. 加载外部类时不会加载内部类,这样可以保证懒加载
 */
public class Mgr07 {
    private static class Mgr07Holder{
        //JVM保证每个CLASS只会LOAD到内存一次
        private final static Mgr07 INSTANCE = new Mgr07();
    }
    //私有构造器
    private Mgr07() {
    }
    /**
     * 懒汉式,获取实例的时候在初始化实例
     * @return
     */
    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }
    /**
     * 如果获取到hashcode一致说明对象是一个对象
     * 这里验证:多线程环境可能创建两个不同实例
     * @param args
     */
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr07 instance = Mgr07.getInstance();
                System.out.println(instance.hashCode());
            }).start();
        }

    }
}

一行代码实现单例-枚举类

Effective JAVA作者推荐写法,个人并不喜欢。我要的类,给我的枚举。

public enum   Mgr08 {
    INSTANCE;
    public static void main(String[] args) {
        for (int i = 0; i <100 ; i++) {
            new Thread(()->{
                Mgr08 instance = Mgr08.INSTANCE;
                System.out.println(instance.hashCode());
            }).start();
        }

    }
}

后记

单例模式使用的场景其实不多,为了面试上面8中写法足够了。如果从技术追求的角度,单例模式从字面上理解即可,掌握第一种写法即可。工作中发现必须用单例的时候用一次就彻底理解了。另外,如果你的代码中大量使用单例模式,就要思考一下你真的懂单例模式吗?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值