当前位置:   article > 正文

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

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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

  • 线程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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

同时,上述代码还有修改的空间,懒汉模式只是在最开始调用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);
    }

}	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 第一个if用于判定是否要加锁
    • 实例化之后,线程自然就安全了,无需加锁了
    • 实例化之前,应该要加锁
  • 第二个if用于判断是否要创建对象
  • 同时volatile也是有必要的,避免触发了优化,避免内存可见性与指令重排序问题
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/431017
推荐阅读
相关标签
  

闽ICP备14008679号