设计模式 —— 单例模式

目录

一、什么是单例设计模式?

二、单例设计模式实现形式

1、饿汉式实现方式一(推荐)

2、饿汉式实现方式二

3、懒汉式实现方式一(线程不安全)

4、懒汉式实现方式二(线程安全,但效率低)

5、懒汉实现方式三(线程不安全)

6、懒汉实现方式四 - DCL 双端检查 + volatile(线程安全)

7、静态内部类方式,JVM 保证单例(推荐)

8、通过枚举实现单例模式(不推荐)


一、什么是单例设计模式?

单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。

类结构图

具体实现

  1. 将构造方法私有化,使其不能在类的外部通过 new关键字实例化该类对象。
  2. 在该类内部产生一个唯一的实例化对象,并且将其封装为 private static 类型。
  3. 定义一个静态方法返回这个唯一对象。

二、单例设计模式实现形式

饿汉式,从名字上也很好理解,就是“比较勤”,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。

懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有新线程安全和线程不安全两种写法,区别就是 Synchronized 关键字。

1、饿汉式实现方式一(推荐

类加载到内存后,就实例化一个单例,JVM保证线程安全;简单使用;

但也有缺点:不管用到与否,类装载时就完成实例化

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 * Class.forName("")
 * (话说你不用的,你装载它干啥)
 */
public class Singleton_01 {

    private static final Singleton_01 INSTANCE = new Singleton_01();

    // 构造方法私有
    private Singleton_01() {}

    public static Singleton_01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Singleton_01 singleton01 = Singleton_01.getInstance();
        Singleton_01 singleton02 = Singleton_01.getInstance();

        System.out.println(singleton01 == singleton02);
    }
}

2、饿汉式实现方式二

与上述 Singleton_01 一样,通过静态代码块实现单例的实例化

/**
 * 跟01是一个意思
 */
public class Singleton_02 {

    private static final Singleton_02 INSTANCE;

    // 通过静态代码块实现单例的实例化
    static {
        INSTANCE = new Singleton_02();
    }

    // 构造方法私有
    private Singleton_02() {}

    public static Singleton_02 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Singleton_02 singleton01 = Singleton_02.getInstance();
        Singleton_02 singleton02 = Singleton_02.getInstance();

        System.out.println(singleton01 == singleton02);
    }

}

3、懒汉式实现方式一(线程不安全

懒汉式即需要使用类的实例化时才创建对象,但下述懒汉式却带来了线程不安全的问题;通过使用多线程也获取单例对象的 hashCode 非常容易拿到不一样的hashCode,证明单例对象不是同一个,即存在线程不安全问题。

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 */
public class Singleton_03 {

    private static Singleton_03 INSTANCE;

    // 构造方法私有
    private Singleton_03() {}

    public static Singleton_03 getInstance() {
        if (INSTANCE == null) {
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (Exception e){
                e.printStackTrace();
            }
            INSTANCE = new Singleton_03();
        }

        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

        }
    }
}

4、懒汉式实现方式二(线程安全,但效率低

运行过 【懒汉式实现方式一】就会发现存在多线程不安全的问题,因为可以引入 Synchronized 同步方法来解决此问题,但却带来了效率下降问题。

补充说明下:Synchronized在这里是修饰静态同步方法,实际上是对该类 Class 对象加锁,俗称“类锁”。

关于: Sychronized 关键字的知识,可参考:Java 并发 —— Synchronized 关键字和锁升级 【可下载 Synchronized_思维导图】

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 线程当安全的问题,可以通过 synchronized 解决,但也带来效率下降
 */
public class Singleton_04 {
    private static Singleton_04 INSTANCE;

    // 构造方法私有
    private Singleton_04() {}

    // 锁住 Singleton_04 对象
    public static synchronized Singleton_04 getInstance() {
        if (INSTANCE == null) {
            try {
                TimeUnit.MILLISECONDS.sleep(1);
            } catch (Exception e){
                e.printStackTrace();
            }
            INSTANCE = new Singleton_04();
        }

        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

        }
    }
}

5、懒汉实现方式三(线程不安全

运行过 【懒汉式实现方式二】就会发现引入 Synchronized 同步方法来解决多线程不安全问题,但却带来了效率下降问题。为此,将 Synchronized 同步锁的粒度减小,发现效率是提升了,但依然没有解决多线程不安全问题

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过 synchronized 解决,但也带来效率下降
 * 通过减少 synchronized 同步锁粒度,发现多线程不安全问题依然存在
 */
public class Singleton_05 {
    private static Singleton_05 INSTANCE;

    // 构造方法私有
    private Singleton_05() {}


    public static Singleton_05 getInstance() {
        if (INSTANCE == null) {
            // 妄图通过减少同步代码块的方式提高效率,然后不可能
            synchronized (Singleton_05.class) {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (Exception e){
                    e.printStackTrace();
                }
                INSTANCE = new Singleton_05();
            }

        }

        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

6、懒汉实现方式四 - DCL 双端检查 + volatile(线程安全

减少 synchronize 效率是可以的,但依然存在多线程不安全问题;通过引入 DCL (Double Check Lock 双端检查方式) + volatile (禁止指令重排),解决了多线程安全问题,同时还提高了效率。

关于volatile 的知识,可参考:请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”

DCL + Volatile,综合了懒汉式和饿汉式两者的优缺点整合而成。看下面代码实现中,特点是在 Synchronized 关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。在这里使用 volatile 会或多或少的影响性能,但考虑到程序的正确性,牺牲这点性能还是值得的。DCL 优点是资源利用率高,第一次执行 getInstance() 时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在高并发环境下也有一定的缺陷,虽然发生的概率很小。DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是它还是在某些情况会出现失效的问题,也就是DCL 失效,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL.

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过 synchronized 解决,但也带来效率下降
 * 通过减少 synchronized 同步锁粒度,发现多线程不安全问题依然存在
 * 通过采用 DCL 即 Double Check Lock 双端检查机制,出现多线程不安全问题概率大大降低了,但还有存在多线程不安全问题
 */
public class Singleton_06 {
    private static volatile Singleton_06 INSTANCE;

    // 构造方法私有
    private Singleton_06() {}


    public static Singleton_06 getInstance() {
        if (INSTANCE == null) {
            // 双重检查
            synchronized (Singleton_06.class) {
                if (INSTANCE == null) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (Exception e){
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton_06();
                }
            }

        }

        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

7、静态内部类方式,JVM 保证单例(推荐

通过 JVM 加载外部类时不会加载内部类,这样可以实现懒加载,完美写法之一,比第一种要好一些
第一次加载 Singleton 类时并不会初始化 INSTANCE,只有第一次调用 getInstance 方法时虚拟机加载 Singleton_07_Holder 并初始化 INSTANCE,这样不仅能确保线程安全也能保证 Singleton 类的唯一性,所以推荐使用静态内部类单例模式。

/**
 * 静态内部类方式
 * JVM 保证单例
 * 加载外部类时不会加载内部类,这样可以实现懒加载
 *
 * 完美写法之一,比第一种要好一些
 */
public class Singleton_07 {

    // 构造方法私有
    private Singleton_07() {}

    private static class Singleton_07_Holder {
        private final static Singleton_07 INSTANCE = new Singleton_07();
    }

    // 不但实现了懒加载,而且只加载一次
    // Singleton_07如果实例化了,Singleton_07_Holder也不会实例化,只有在getInstance被调用时才加载
    public static Singleton_07 getInstance() {
        return Singleton_07_Holder.INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

8、通过枚举实现单例模式(不推荐

枚举的方式是比较少见的一种实现方式,但是看上面的代码实现,却更简洁清晰。不仅可以解决线程同步,还可以防止反序列化.

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现在,有一种情况下他们会重新创建对象,那就是反序化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了 readResolve 方法,这个方法可以让开发人员控制对象的反序列化。在上述几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象。

枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。

/**
 * 不仅可以解决线程同步,还可以防止反序列化
 */
public enum Singleton_08 {

    INSTANCE;

    public void m() {}

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

总结:

上述8种写法,至于在实际项目中选择哪种形式的单例模式,取决于你的项目本身,是否有复杂的并发环境,还是需要控制单例对象的资源消耗。

 


文章最后,给大家推荐一些受欢迎的技术博客链接

  1. JAVA相关的深度技术博客链接
  2. Flink 相关技术博客链接
  3. Spark 核心技术链接
  4. 设计模式 —— 深度技术博客链接
  5. 机器学习 —— 深度技术博客链接
  6. Hadoop相关技术博客链接
  7. 超全干货--Flink思维导图,花了3周左右编写、校对
  8. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  9. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  10. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
  11. 深入聊聊Java 垃圾回收机制【附原理图及调优方法】

欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不埋雷的探长

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

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

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

打赏作者

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

抵扣说明:

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

余额充值