当前位置: 首页 > article >正文

Java单例模式写法

目录

  • 单例模式
    • 饿汉模式实现单例
    • 懒汉模式实现单例
      • 单线程版
      • 多线程版
      • 多线程版优化
  • 小结

单例模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
为什么要保证只存在一份对象呢?
因为有些对象管理的内存数据可能会很多, 可能有些项目里就一个对象运行起来就吃上百G的内存空间, 如果不小心多new了几个, 那系统可能直接崩溃了.

饿汉模式实现单例

类加载的同时, 创建实例.

class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {}    //不允许外部调用构造方法
    public static Singleton getSingleton() {
        return singleton;  //将创建好的实例返回
    }
}

这里只是单纯的读操作, 因此该模式是线程安全的.

懒汉模式实现单例

核心思想 :非必要不创建.
加载的时候不创建实例. 第一次使用的时候才创建实例.

单线程版

class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}  //修改了构造方法的访问修饰权限符, 只有在类内部才能访问构造方法
    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

上面的懒汉模式实现是线程不安全的.
为什么不安全呢?
比如两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 它两都 new 出了对象, 这与我们预期的创建一个对象不符, 所以线程不安全.

我们可以通过加锁来解决这一问题, 下面是多线程版 , 线程安全.

多线程版

怎么加锁呢?
看看这样加锁是否可行:

class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if(singleton == null) {
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

显然这样是不行的, 当两个线程同时调用 getSingleton 方法时, 此时 singleton 还为空, t1 线程和 t2 线程都走进了判断语句, 判断通过, 然后通过竞争锁, 竞争成功的线程先 new 对象, 另一个线程后 new 对象, 这也是 new 了多个对象, 与预期不符.

我们再来看看给方法加锁:

class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public synchronized static Singleton getSingleton() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

当多个线程同时调用 getSingleton 方法时, 通过竞争锁, 竞争成功的线程先进去创建完对象出来后, 其他线程再来获取对象就不会再创建对象了.

其实这里还有一个问题, 那就是指令重排序问题, 什么意思呢?
举个例字 :
假设我们有两个线程 t1 和 t2,
t1 有个操作是: s = new Student();
t2 有个操作是: if(s != null) { s.learn(); }

这个操作 : s = new Student(); 大体可以分为三个步骤:

  1. 申请内存空间
  2. 调用构造方法(初始化内存的数据)
  3. 把对象的引用赋值给 s (内存地址的赋值)

如果是单线程, 此处进行指令重排序,步骤2和步骤3是可以调换顺序的, 重排序后可能就是132执行了, 这对单线程结果没影响, 但多线程就不行了.

回到上面的 t1 和 t2, 如果 t1 的操作进行指令重排序, 就会先申请内存, 然后把这个内存地址赋值给 s (注意这里还没有调用构造方法, 没有new对象), 这时 s 还是 null ,这个时候如果线程 t2 刚好进行 if 判断则会直接进入, 然后调用 s 的方法, 因为 s 为空, 就会抛出空指针异常.

这也是上面给方法加锁代码存在的问题, 如果得到的 singleton 为空就调用这里面的方法, 那就会产生空指针异常.

如何避免发生指令重排序呢?
我们可以加个 volatile 来禁止指令重排序, 在下面代码中体现.

多线程版优化

class Singleton {
    private volatile static Singleton singleton; //禁止对singleton进行指令重排序
    private Singleton() {}
    public static Singleton getSingleton() {
        //注意这里有两个 if 判断是否为空!!! 
        if(singleton == null) {
            synchronized (Singleton.class) {
                if(singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

为什么要两个 if 判断呢?
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了.
这样可以让加锁操作只在第一次创建实例的时候出现.

小结

单例模式线程安全问题 :

  1. 饿汉模式, 天然就是安全的, 只是读操作.

  2. 懒汉模式, 不安全的, 有读也有写.

如何将懒汉模式变安全:

  1. 加锁, 把 if new 变为原子操作.
  2. 双重 if, 减少不必要的加锁操作.
  3. 使用 volatile 禁止指令重排序, 保证后续线程拿到的是完整对象.

http://www.kler.cn/a/1879.html

相关文章:

  • Kafka快速扫描
  • 数据库操作【JDBC HIbernate Mybatis】
  • Odoo:免费开源ERP的AI技术赋能出海企业电子商务应用介绍
  • 深入了解蓝牙Profile类型与设备的对应关系
  • 叉车作业如何确认安全距离——UWB测距防撞系统的应用
  • C++中的模板元编程
  • 差分运放公式推导-运算放大器
  • 初阶C语言:冒泡排序
  • typescript(元组、枚举、类、泛型)
  • mysql数据库常问面试题
  • 我的 System Verilog 学习记录(10)
  • CF1770E Koxia and Tree
  • 探索css渐变-实现饼图-加载图-灯柱
  • 【Java】UDP网络编程
  • 蓝桥杯算法全集之完全背包问题(动态规划算法)
  • 蓝桥杯真题——自动售水机
  • LeetCode:704. 二分查找
  • 区块链基本原理
  • 【jvm】JVM(三)JVM 垃圾回收算法详解(CMS、三色标记)
  • 【进阶数据结构】二叉搜索树经典习题讲解
  • Python读写EXCEL文件常用方法大全
  • ChatGPT加强版GPT-4面世,打工人的方式将被颠覆
  • 遗传算法原理及案例解析
  • SAP BPC简介
  • vue大型商城系统中遇到的问题(上)
  • 【链表OJ题(九)】环形链表延伸问题以及相关OJ题