JavaEE初阶Day 9:多线程(7)

Day 9:多线程(7)

多线程代码案例

1. 案例一:单例模式

单例模式是一种经典的设计模式,面试中非常常见

设计模式类似于“棋谱”,将编程中各种经典的问题场景进行整理并且提供一些解决方案;设计模式其实有很多种,绝对不止23种,随着时代的变化,新的设计模式不断地诞生,旧的模式也就在消亡

简单来说,单例模式就是单个实例

  • 整个进程中的某个类,有且只有一个对象,并不会new出多个对象,这样的对象,称为单例
  • 但是如何保证这个类只有一个实例呢,靠程序员口头保证肯定是不可行的
  • 需要让编译器来帮我们做一个强制的检查,通过一些编码上的技巧,使编译器可以自动发现代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错

代码中的有些对象,本身就不应该是有多个实例的,从业务角度就应该是单个实例

  • 比如,写的服务器,要从硬盘上加载100G的数据到内存中(加载到若干个哈希表里)

    肯定要写一个类,封装上述加载操作,并且写一些获取/处理数据的业务逻辑

    这样的类,就应该是单例的,一个实例,就管理100G的内存数据,搞多个实例就是N*100G的内存数据,机器吃不消也没必要

  • 再比如,服务器也可能涉及到一些“配置项”(MySQL有配置文件)

    代码中也需要有专门的类,管理配置,需要加载配置数据到内存中供其他代码使用

    这样的类的实例也应该是单例的,如果是多个实例,就存储了多份数据,如果一样还可以接受,如果不一样,以哪一个为准?

根本上保证对象是唯一实例,这样的代码,就称为单例模式,单例模式有很多不同的写法,下面主要介绍两种:饿汉模式与懒汉模式

1.1 饿汉模式
package thread;

class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }

    private Singleton(){

    }
}

public class Demo27 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1==s2);
    }
}
  • private static Singleton instance = new Singleton();
    • static成员初始化时机是在类加载的时候,此处可以简单地认为,JVM已启动,就立即加载(具体情况可能有变数)
    • static修饰的,其实是**“类属性”**,就是在“类对象”上的,每个类的类对象在JVM中只有一份
    • 此时的Singleton类只存在一个实例instance,初始化的时候只执行一次
  • getInstance():此处后续需要使用这个类的实例,就可以通过getInstance()来获取已经new好的这个实例,而不是重新实例化
  • private Singleton(){ }
    • 这样的private构造方法可以防止其他代码重新实例化这个类
    • 类之外的代码,尝试实例化的时候,就必须调用构造方法,由于构造方法是私有的,无法调用,就会编译出错
1.2 懒汉模式

懒在计算机中往往是一个褒义词,而且是高效率的代表

懒汉模式不是在程序启动的时候创建实例,而是在第一次使用的时候才去创建

如果不使用了,就会把创建实例的代码节省下来了

如果代码中存在多个单例类

  • 使用饿汉模式,就会导致这些实例都是在程序启动的时候扎堆创建的,可能会把程序启动时间拖慢,
  • 使用懒汉模式,什么时候首次调用,调用时机是分散的,化整为零,用户不太容易感知到卡顿
package thread;


class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy() {

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}

if (instance == null) {instance = new SingletonLazy();}:什么时候调用就什么时候创建,如果不调用,就不创建了

1.3 线程安全问题

上述两种单例模式,是否是线程安全的

考虑有多个线程同时调用getInstance,是否会产生线程安全问题

  • 对于饿汉模式

    • 创建实例的时机是在Java进程启动,比main调用还早的时机
    • 后续代码里创建线程,一定比上述实例创建要更迟
    • 后续执行getInstance的时候,意味着上述实例早都已经有了
    • 每个线程的getInstance只做了一件事,就是读取上述静态变量的值
    • 多个线程读取同一个变量,是线程安全的
  • 对于懒汉模式

    • if (instance == null) {instance = new SingletonLazy();}:这行代码可以理解为,包含了读和写
    • 读:查看一下instance变量的值
    • if条件判定,拿出来instance里面的引用的地址,看一下是否为null
    • 写:修改,赋值就是修改
    • 上述代码在多线程环境下就可能产生问题
1.4 懒汉模式修改

上述的懒汉模式造成的多线程不安全问题,本质是因为,如果执行顺序如下:

  • 线程t1判断了instance为null后
  • 此时,线程t2也进行了判断,同样认为instance为null
  • 接下来线程t1开始创建实例
  • 由于t2之前判断instance为null,于是也创建实例
  • 最后导致第二个对象的地址覆盖了第一个

那么我们通过加锁的方式来进行尝试

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

但是上述代码仍然存在问题:

  • 线程t1判断了instance为null后
  • 此时,线程t2也进行了判断,同样认为instance为null
  • 接下来线程t1进行加锁,之后,线程t1开始创建实例,并释放锁
  • t2拿到锁之后,由于t2之前判断instance为null,于是也创建实例
  • 最后同样导致第二个对象的地址覆盖了第一个

于是应该把if和new打包成一个原子操作

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

同时,上述代码还有修改的空间,懒汉模式只是在最开始调用getInstance会存在线程安全问题,一旦把实例创建好了,后续再调用,就只是读操作了,就不存在线程安全问题了,针对后续调用,明明没有线程安全问题,还要加锁,就是画蛇添足(加锁本身,也是有开销的,可能会使线程阻塞)

代码如下:

package thread;


class SingletonLazy {
    private static volatile SingletonLazy instance = null;
    private static Object locker = new Object();

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

    private SingletonLazy() {

    }
}
public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }

}	
  • 第一个if用于判定是否要加锁
    • 实例化之后,线程自然就安全了,无需加锁了
    • 实例化之前,应该要加锁
  • 第二个if用于判断是否要创建对象
  • 同时volatile也是有必要的,避免触发了优化,避免内存可见性与指令重排序问题
  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

胖了你都蹲不下来撸猫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值