奇思妙想录 一日不读口生,一日不写手生。
歌曲封面 未知作品

萌ICP备20248808号

津ICP备2023004371号-2

网站已运行 2 年 130 天 6 小时 41 分

Powered by Typecho & Sunny

8 online · 92 ms

Title

JavaEE多线程案例

IhaveBB

·

·

91次阅读
技术分享
Article
⚠️ 本文最后更新于2024年01月21日,已经过了305天没有更新,若内容或图片失效,请留言反馈
AI摘要:本文介绍了JavaEE中的多线程案例,重点讲解了单例模式的实现方式,包括饿汉模式和懒汉模式,并分析了它们在多线程环境下的线程安全问题。通过加锁和使用volatile关键字,解决了懒汉模式的线程安全问题。同时,文章还简要介绍了阻塞队列的概念和特点,强调了其在生产者消费者模型中的应用。

Powered by AISummary.

多线程案例

一.单例模式

1.1单例模式介绍

单例模式是一个非常经典的设计模式

既然我们提到了设计模式,那么什么是设计模式呢?

设计模式,就是我们程序猿的棋谱,我们在实际开发过程中,如果遇到一些特定的经典场景,我们就可以按照这个解决方案来进行编码

校招中,考察的设计模式最常见的有两个

  1. 单例模式
  2. 工厂模式

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。

在有些场景中,我们希望有的类只能有一个对象,不能有多个,这时候我们就可以使用单例模式了。

看到这里大家可能会有一个疑问:我只new一次对象不就好了吗,为什么要这么复杂?

单例模式的重要性

我们需要让编译器帮我们监督,确保这个类不会new出多个对象。如果只靠注释标明这个类不能创建多个对象那是万万不能的。

这样的思想方法,我们之前学习过的很多地方也都有涉及,比如下面这四个。

  • final
  • interface
  • @Override
  • throws.

但是在语法层面上,并没有对单例模式做出支持,所以我们就只能通过一些编程技巧,来达成类似的效果。

接下来我们详细介绍一下单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种.

1.2饿汉模式(基础)

1.2.1代码描述

  1. 私有构造函数:类Singleton有一个私有构造函数,这意味着在类外部无法使用new关键字创建其实例。这强制要求获取Singleton类的唯一实例只能通过getInstance方法来实现。
  2. 静态实例变量:该类有一个私有的静态实例变量instance,它保存了该类的唯一实例。它在类加载时初始化,因为它在一行中声明和实例化。
  3. 静态getInstance方法getInstance方法是公共的和静态的,允许外部代码获取Singleton类的唯一实例。该方法返回instance变量。

1.2.2代码实现

♾️ java 代码:
public class Singleton {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() {}
}

1.2.3 引用测试

♾️ java 代码:
public class Demo1 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);
        System.out.println(singleton1.equals(singleton2));
    }
}
image-20230919215704205

调用代码后我们发现,singleton1和singleton2实际上引用了同一个对象。这就是Singleton模式的主要作用:确保在整个应用程序中只有一个实例,并提供一种全局的方式来访问它。

1.3饱汉模式(基础)

1.3.1 代码描述

  1. 私有构造函数:类SingletonLazy有一个私有构造函数,这阻止了在类外部通过new关键字创建类的实例。
  2. 静态实例变量:类中有一个私有的静态成员变量INSTANCE,用于存储类的唯一实例。初始时,它被设置为null
  3. 静态getInstance方法getInstance方法是公共的和静态的,用于获取SingletonLazy类的唯一实例。在这个方法中,检查INSTANCE是否为null。如果INSTANCEnull,则创建一个新的SingletonLazy实例并将其分配给INSTANCE,然后返回该实例。如果INSTANCE不是null,则直接返回已经存在的实例。

1.3.2代码实现

♾️ java 代码:
public class SingletonLazy {
    private static SingletonLazy INSTANCE = null;

    private SingletonLazy() {}

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

1.3.3 引用测试

♾️ java 代码:
public class Demo1 {
    public static void main(String[] args) {
        SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
        SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
        System.out.println(singletonLazy1 == singletonLazy2);
        System.out.println(singletonLazy1.equals(singletonLazy2));
    }
}

1.4 线程安全问题

回看我们这一章的标题”多线程“,我们来思考一下这两种写法,是否存在线程安全问题?(多个线程同时调用getinstance是否能出现问题?)

那么让我们回顾一下怎样才能构成线程安全问题;

如果多个线程,同时修改同一个变量就可能构成线程安全问题

如果多个线程,同时读取一个变量就不会构成线程安全难问题

看到这里,大家心里可能已经有了答案:饿汉模式没有线程安全问题、懒汉模式具有线程安全问题。

因为饿汉模式只进行了读取,没有进行修改:而懒汉模式既会读取又会修改。


那么我们应该怎么做呢?加锁!

♾️ java 代码:
public static SingletonLazy getInstance() {
    synchronized (SingletonLazy.class) {
        if (INSTANCE == null) {
            INSTANCE = new SingletonLazy();
        }
    }
    return INSTANCE;
}

我们按照这个方式给代码加锁之后,确是能解决部分多线程修改的问题,但是一旦代码这么写,后续每次调用getinstance,都需要先加锁。但是实际上,懒汉模式中线程安全问题只出现在最开始还没有new对象的时候。一旦对象new出来了,后续多线程调用getinstance就只有读取,就不会有线程安全问题了。

所以我们应该如何修改提升代码的性能呢?

我们上面提到,懒汉模式只有在第一次new对象的时候才会加锁,那么我们可以在加锁操作前进行一次if判断,如果对象已经创建了,线程就安全了,此时就不需要加锁了

♾️ java 代码:
 public static SingletonLazy getInstance() {
        if(INSTANCE == null) {
            synchronized (SingletonLazy.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SingletonLazy();
                }
            }
        }
        return INSTANCE;
    }

第一个iff用来判断是否需要加锁

第二个if用来判断是否需要new对象

我们平常很少写出这样的语句。在这里只不过是凑巧,这俩判断是一样的写法。


代码写到这里,我们还需要注意一个非常重要的问题指令重排序

我们之前提到过,编译器为了提高执行效率,可能调整原代码的执行顺序。

image-20230919234800249

就比如我们去超市,想买白菜→韭菜→西红柿→蒜薹

但是实际中按这样的顺序去超市购买会来来回回的跑,非常复杂。

编译器就可能帮我们优化成西红柿→蒜薹→白菜→韭菜

通常情况下,指令重排序能够在保证逻辑不变的前提下,优化程序的执行速度

但是在我们上面的这个代码中,就可能会出现一些问题

new操作,是可能触发指令重排序的

new操作可以拆分为散步:

  1. 申请内存空间
  2. 在内存空间上构造对象(构造方法)
  3. 把内存的地址,赋值给instance引用

可以按照1 2 3的顺序来执行,也可以按照1 3 2的顺序来执行。(1一定是最先执行的)

其实在单线程中哪一种执行方式都是无所谓的,但是在多线程中就会有问题了

eg执行顺序①③②

当线程一执行完1和3时,Instance变成非NULL了,但是Instance指向的是一个还没有初始化的对象

此时线程二开始执行,由于我们第一个 if(INSTANCE == null)没有加锁,所以可以线程二判定instance == NULL不成立,于是线程二返回INSTANCE。进一步线程二线程代码就可能访问INSTANCE里的方法和属性

所以我们针对这个代码加一个volatile即可解决。

♾️ java 代码:
public class SingletonLazy {
    private static volatile SingletonLazy INSTANCE = null;
       private static SingletonLazy() {}

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

1.5阻塞队列

阻塞队列是一种特殊的队列,其最大的意义就是实现“生产者消费者模型”

  1. 线程安全
  2. 带有阻塞特性

    1. 如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列里添加元素为止
    2. 如果队列为满,继续如队列,也会发生阻塞,阻塞到其他线程从队列中取走元素为止
现在已有 0 条评论,0 人点赞
Comment:共0条
发表
搜 索 消 息 足 迹
你还不曾留言过..
你还不曾留下足迹..
博主

哈喽大家好呀

本站已从原站:33mc.cn
迁移到本站:blog.nicebao.com
主域名变为:nicebao.com

不再显示
博主