Java设计模式-单例模式-饿汉-懒汉-饿汉线程不安全解决

目录
  1. 为什么需要单例
  2. 饿汉模式的简单实现
  3. 懒汉模式的简单实现
  4. 二者比较
  5. 解决懒汉模式的线程不安全问题
为什么需要单例

单例模式能够保证一个类仅有唯一的实例,并提供一个全局访问点。

我们是不是可以通过一个全局变量来实现单例模式的要求呢?我们只要仔细地想想看,全局变量确实可以提供一个全局访问点,但是它不能防止别人实例化多个对象。通过外部程序来控制的对象的产生的个数,势必会系统的增加管理成本,增大模块之间的耦合度。所以,最好的解决办法就是让类自己负责保存它的唯一实例,并且让这个类保证不会产生第二个实例,同时提供一个让外部对象访问该实例的方法。自己的事情自己办,而不是由别人代办,这非常符合面向对象的封装原则。

单例模式主要有3个特点:

  1. 单例类确保自己只有一个实例。
  2. 单例类必须自己创建自己的实例。
  3. 单例类必须为其他对象提供唯一的实例。
饿汉模式

实现单例的饿汉模式主要有3步:

  1. 让默认的构造函数私有化,使外部类无法通过new的方式获取该类的实例
private SingletonHunger() {}
  1. 我们依旧要提供实例给外部,而外部类又无法取得该类的实例对象,所以我们将实例以静态变量的方式提供
public static SingletonHunger instance = new SingletonHunger();
public static void main(String[] args) {
    SingletonHunger s1 = SingletonHunger.instance;
    SingletonHunger s2 = SingletonHunger.instance;
    System.out.println(s1 == s2); // true
}
  1. 将instance类变量私有化并提供getter访问器
private static SingletonHunger instance = new SingletonHunger();

public static SingletonHunger getInstance() {
    return instance;
}

饿汉模式有什么特点呢,可以看到最明显的就是instance是静态成员变量,它在类被加载的时候就会被实例化,不管有没有被其它外部类访问,所以这种一开始就实例化好的,我们觉得它很着急,所以叫饿汉模式。

public class SingletonHunger {

    // 1. 将默认的构造函数私有化
    private SingletonHunger() {}

    // 2. 提供静态变量
    private static SingletonHunger instance = new SingletonHunger();

    // 3. 创建instance变量的getter访问器
    public static SingletonHunger getInstance() {
        return instance;
    }
}
懒汉模式

懒汉模式和饿汉模式的却别就在于懒汉模式只是声明类的实例变量,而在有外部类(线程)访问的时候才去真正实例化(开辟内存空间),之后的所有线程都共享最先创建的那个实例

public class SingletonLazy {

    // 1. 将默认的构造函数私有化
    private SingletonLazy() {
    }

    // 2. 声明类的唯一实例 只是声明
    private static SingletonLazy instance;

    // 3. 为instance提供访问器
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}
二者比较

饿汉模式:类加载时较慢,访问对象时较快,线程安全

懒汉模式:类加载时较快,访问对象时较慢,线程不安全

大多数应用场景下都使用懒汉模式,因为饿汉模式还有一个问题那便是内存空间的浪费。所以我们就需要解决懒汉模式的线程不安全问题

解决懒汉模式的线程不安全问题

主要解决的问题是两个

  1. 线程的并发
  2. JVM的指令重排

第一个问题我们可以用加锁来实现,用sychronizd即可,第二个问题是Java的关键字volatile。

**加锁:**我们可以直接加在getter上,但是这样子的静态方法锁整个临界区比较大,比较耗费资源,所以使用同步代码块

public static SingletonLazy getInstance() {
    if (instance == null) {
        synchronized (SingletonLazy.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
    }
    return instance;
}

为什么要判空两次?其实就是为了用同步代码块,你必须保证临界区完成一整套必不可少的操作,最开始的判空只是判断是否需要进入临界区。加入两个线程同时停在了第一个判空处,其中一个线程获得锁进去不判空直接new,那么它完成操作释放锁之后对于第二个等待锁的线程而言,它获得一释放的锁之后也是进去直接new,很显然,这一点都不符合临界区的设计。

volatile

instance = new Singleton();

这条语句并不是一个原子操作

  1. 分配内存给对象,在内存中开辟一段地址空间;// Singleton var = new Singleton();
  2. 对象的初始化;// var = init();
  3. 将分配好对象的地址指向instance变量,即instance变量的写;// instance = var;
    设想一下,如果不使用volatile关键字限制指令的重排序,1-3-2操作,获得到单例的线程可能拿到了一个空对象,后续操作会有影响!因此需要引入volatile对变量进行修饰。

再详细的内容参考博客

所以最后我们得到的结果为这样

public class SingletonLazy {

    // 1. 将默认的构造函数私有化
    private SingletonLazy() {
    }

    // 2. 声明类的唯一实例 只是声明
    private volatile static SingletonLazy instance;

    // 3. 为instance提供访问器

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}
参考博客

java单例设计模式详解(懒汉饿汉式)+深入分析为什么懒汉式是线程不安全的+解决办法:https://blog.csdn.net/yaoyaoyao_123/article/details/84799861

【JAVA】线程安全的懒汉模式为什么要使用volatile关键字:https://blog.csdn.net/weixin_42078452/article/details/84892372

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值