【多线程】多线程案例

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇
✨每日一语:we can not judge the value of a moment until it becomes a memory.

目 录

  • 🍝一. 单例模式
    • 🍤1. 饿汉模式实现
    • 🦪2. 懒汉模式实现
  • 🍲二. 阻塞式队列


🍝一. 单例模式

单例模式是校招中最常考的设计模式之一.

啥是设计模式?

设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
 
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

单例模式目的:有些对象,在一个程序中应该只有唯一一个实例,就可以使用单例模式。单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。

一个程序中应该只有唯一一个实例是程序猿来保证的,不一定靠谱,于是在单例模式下借助语法,强行限制咱们不能创建多个实例。

Java 里的单例模式,有很多种实现方式,主要介绍两个大类:饿汉模式懒汉模式

饿汉模式:程序启动,则立即创建实例

懒汉模式:程序启动,先不着急创建实例,等到真正用的时候,再创建


单例模式具体实现方式:

🍤1. 饿汉模式实现

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

    public static Singleton getInstance(){
         return instance;
    }

    //构造方法设为私有!其他的类想来 new 就不行了
    private Singleton(){ }
}

public class Demo19 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton isstance2 = Singleton.getInstance();
        System.out.println(instance == isstance2);
    }
}
  1. static 静态,实际效果和字面意思没有任何关联,实际的含义是:类属性 / 类方法。
  2. 类属性就长在类对象上,类对象在整个程序中只有唯一一个实例!!(JVM 保证的)
  3. 在静态方法中,后续我们需要使用这个实例,统一基于 getInstance 方法来获取,实例独苗,不需要再 new 了(new 也会失败)
  4. 使用静态成员表示实例(唯一性) + 让构造方法为私有(堵住了 new 创建新实例的口子)

按照现在的代码,当 Singleton 类被加载的时候,就会执行到此处的实例化操作!!实例化时机非常早!(非常迫切的感觉)


🦪2. 懒汉模式实现

class Singletonlazy{
    private static Singletonlazy instance = null;

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

    private Singletonlazy(){ }
}
  1. 第一步并没有创建实例!
  2. 首次调用 getInstance 才会创建实例!

上面俩种模式还涉及到线程安全。

  • 饿汉模式是线程安全的,多线程涉及 getInstance ,只是多线程读,没事儿
  • 懒汉模式是线程不安全的,有的地方在读,有的地方在写,一旦实例创建好了之后,后续 if 条件就进不去了,此时也就全是读操作了,也就线程安全了。

如何解决懒汉模式线程不安全?

方法就是加锁:

synchronized (Singletonlazy.class) {
    if (instance == null) {
        instance = new Singletonlazy();
    }
}

把读和写两个步骤打包在一起,保证读 判定 修改 这组操作是原子的!

懒汉模式,只是初始情况下,才会有线程不安全问题,一旦实例创建好了之后,此时就安全了!既然如此,后续在调用 getlnstance 的时候就不应该再尝试加锁了!当线程安全之后,再尝试加锁,就非常影响效率了。

如上代码我们只需要再嵌套一个 if 判定即可

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

注意!不要用单线程的理解方式来看待多线程代码!如果是单线程,连续两个一样的 if 判定,毫无意义!但是多线程就不是了,尤其是中间隔了个加锁操作!

  • 加锁操作可能就涉及到阻塞,前面的 if 和后面的 if 中间可能就隔了个 “沧海桑田”。
  • 外层 if 判定当前是否已经初始化好,如果未初始化好,就尝试加锁,如果是已初始化好,那么就直接往下走。
  • 里层的 if 是在多个线程尝试初始化,产生了锁竞争,这些参与锁竞争的线程,拿到锁之后,再进一步确认,是否真的要初始化。

理解双重 if 判定:最核心的目标,就是降低锁竞争的概率
 
当多线程首次调用 getInstance,发现 instance 为 null, 于是又继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作。当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例

  1. 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.
  2. 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来.
  3. 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.
  4. 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.

很多线程尝试读,这样的读,是否会被优化成读寄存器呢?

第一个线程读,把内存的数据读到寄存器了,第二个线程也去读,会不会就直接重复利用上述寄存器的结果呢?由于每个线程有自己的上下文,每个线程有自己的寄存器内容,因此按理来说是不会出现优化的,但是实际上不一定,因此在这个场景下,给 instance 加上 volatile 是最稳健的做法!

volatile private static Singletonlazy instance = null;

对懒汉模式的总结:

  • 加锁
  • 双重 if 判定(外层 if 为了降低加锁的频率,降低锁冲突的概率,里层 if 才是真正判定是否要实例化)
  • volatile

🍲二. 阻塞式队列

阻塞队列是什么?

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
  • 阻塞队列:能够保证 “线程安全”
  • 无锁队列:也是一种线程安全的队列,实现内部没有使用锁,更高效,消耗更多的 CPU 资源。
  • 消息队列:在队列中涵盖多种不同 “类型” 元素,取元素的时候可以按照某个类型来取,做到针对该类型的 “先进先出” (甚至说会把消息队列作为服务器,单独部署)

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型


生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。

优点:

  1. 可以更好地做到 “解耦合”。

在这里插入图片描述
A 直接给 B 发送数据,就是耦合性比较强,开发 A 的时候就得考虑 B 是如何接收的,开发 B 的时候就得考虑 A 是如何发送的。极端情况下 A 出现问题挂了 可以能也造成 B 出现问题导致 B 也挂了,反之 B 出现了问题,也会牵连 A 导致 A 挂了。
 
于是在阻塞队列的影响下,A 和 B 不再直接交互在这里插入图片描述
开发阶段:A 只用考虑自己和队列如何交互,B 也只用考虑自己和队列如何交互,A 和 B 之间都不需要知道对方的存在。
部署阶段:A 如果挂了,对 B 没有任何影响;B 如果挂了,对 A 没有任何影响。

  1. 能够做到 “削峰填谷” ,提高整个系统抗风险能力。

在这里插入图片描述
程序猿无法控制外网有多少个用户在访问 A,当出现极端情况,外网访问请求大量涌入的时候,A 把所有请求的数据一并转让给 B 的时候,B 就容易扛不住而挂掉。
 
在阻塞队列的影响下在这里插入图片描述
多出来的压力队列承担了,队列里多存一会儿数据就行了,即使 A 的压力比较大,B 仍按照固定的频率来取数据。


标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

生产者消费者模型:

public class Demo20 {
    public static void main(String[] args) {
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();

        Thread customer = new Thread(()->{
           while (true){
               try {
                   int value = queue.take();
                   System.out.println("消费元素:" + value);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        customer.start();

        Thread producer = new Thread(()->{
            int n = 0;
            while (true){
                try {
                    System.out.println("生产元素:" + n);
                    queue.put(n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

运行结果演示图:
在这里插入图片描述

阻塞队列实现:

  • 自己模拟实现一个阻塞队列
  • 基于数组的方式来实现队列
  • 两个核心方法:1. put 入队列; 2. take 出队列
class MyBlockingQueue {
    // 假定最大是 1000 个元素,当然也可以设定成可配置的
    private int[] items = new int[1000];
    //队首的位置
    private int head = 0;
    //队尾的位置
    private int tail = 0;
    //队列的元素个数
    private int size = 0;

    //入队列
    public void put (int value) throws InterruptedException {
        synchronized (this) {
            while (size == items.length) {
                //队列已满,继续等待
                this.wait();
            }
            items[tail] = value;
            tail++;
            if (tail == items.length) {
                //注意 如果 tail 到达数组末尾,就需要从头开始
                tail = 0;
            }
            size++;
            //即使没人在等待,多调用几次 notify 也没事,没负面影响
            this.notify();
        }
    }

    //出队列
    public Integer take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while (size == 0) {
                //队列为空,就等待
                this.wait();
            }
            ret = items[head];
            head++;
            if (head == items.length) {
                head = 0;
            }
            size--;
            this.notify();
        }
        return ret;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(100);
        queue.take();
    }
}
  • 入队列中的 wait出队列中的 notify 对应,满了之后,入队列就要阻塞等待,此时在取走元素之后,就可以尝试唤醒了。
  • 入队列中的 notify出队列中的 wait 对应,队列为空,也要阻塞,此时在插入成功之后,队列就不为空了,就能够把 take 的等待唤醒。
  • 一个线程中无法做到又等待又唤醒
  • 阻塞之后,就要唤醒,阻塞和唤醒之间是沧海桑田,虽然按照当下代码是有元素插入成功了,条件不成立,等待结束。但是更稳妥的做法是把 if 换成 while ,在唤醒之后,再判断一次条件!万一条件又成立了呢?万一接下来要继续阻塞等待呢?

测试代码:

public class Demo21 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread customer = new Thread(()->{
           while (true){
               int value = 0;
               try {
                   value = queue.take();
                   System.out.println("消费:" + value);
                   Thread.sleep(500);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        customer.start();

        Thread producer = new Thread(()->{
           int value = 0;
           while (true){
               try {
                   queue.put(value);
                   System.out.println("生产:" + value);
                   value++;
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        producer.start();
    }
}

延缓了消费代码,也可以把生产代码延缓,调用 sleep 即可
 

  • 延缓消费代码
    在这里插入图片描述
  • 延缓生产代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/1745.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

java如何创建线程

java如何创建线程1. java如何创建线程1.1 通过继承Thread类来创建线程1.2 通过实现Runnable接口来创建线程1.3 通过匿名内部类来创建线程1.4 lambda表达式1.5 通过实现Runnable接口的方式创建线程目标类的优缺点1. java如何创建线程 一个线程在Java中使用一个Thread实例来描述…

android8 rk3399 同时支持多个USB摄像头

文章目录一、前文二、CameraHal_Module.h三、CameraHal_Module.cpp四、编译&烧录Image五、App验证一、前文 Android系统默认支持2个摄像头&#xff0c;一个前置摄像头&#xff0c;一个后置摄像头需要支持数量更多的摄像头&#xff0c;得修改Android Hal层的代码 二、Camer…

VueX快速入门(适合后端,无脑入门!!!)

文章目录前言State和Mutations基础简化gettersMutationsActions&#xff08;异步&#xff09;Module总结前言 作为一个没啥前端基础&#xff08;就是那种跳过js直接学vue的那种。。。&#xff09;的后端选手。按照自己的思路总结了一下对VueX的理解。大佬勿喷qAq。 首先我们需要…

我的 System Verilog 学习记录(11)

引言 本文简单介绍 SystemVerilog 的其他程序结构。 前文链接&#xff1a; 我的 System Verilog 学习记录&#xff08;1&#xff09; 我的 System Verilog 学习记录&#xff08;2&#xff09; 我的 System Verilog 学习记录&#xff08;3&#xff09; 我的 System Verilo…

Linux lvm管理讲解及命令

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的绽放&#xff0…

软件行业的最后十年【ChatGPT】

在这篇文章中&#xff0c;我将说明像 ChatGPT 这样的生成式人工智能 (GAI) 将如何在十年内取代软件工程师。 预测被离散化为 5 个阶段&#xff0c;总体轨迹趋向于完全接管。 但首先&#xff0c;一个简短的前言。 推荐&#xff1a;用 NSDT场景设计器 快速搭建3D场景。 1、关于AI…

二叉搜索树:AVL平衡

文章目录一、 二叉搜索树1.1 概念1.2 操作1.3 代码实现二、二叉搜索树的应用K模型和KV模型三、二叉搜索树的性能分析四、AVL树4.1 AVL树的概念4.2 AVL树的实现原理4.3 旋转4.4 AVL树最终代码一、 二叉搜索树 1.1 概念 二叉搜索树&#xff08; Binary Search Tree&#xff0c;…

LeetCode刷题记录---数位DP算法

😄 学会数位dp算法,可以连杀好几道力扣困难题,加油~ 🚀题目: 难度题目困难2376. 统计特殊整数困难1012. 至少有 1 位重复的数字困难233. 数字 1 的个数困难面试题 17.06. 2出现的次数🚀学习资料: 数位dp算法,我是跟着灵神学的,感谢灵神!数位 dp 通用模板参考灵神…

Python数据分析案例24——基于深度学习的锂电池寿命预测

本期开始案例较为硬核起来了&#xff0c;适合理工科的硕士&#xff0c;人文社科的同学可以看前面的案例。 案例背景 这篇文章是去年就发了&#xff0c;刊物也印刷了&#xff0c;现在分享一部分代码作为案例给需要的同学。 原文链接&#xff08;知网文章 C核&#xff09;&…

python如何快速采集美~女视频?无反爬

人生苦短 我用python~ 这次康康能给大家整点好看的不~ 环境使用: Python 3.8 Pycharm mou歌浏览器 mou歌驱动 —> 驱动版本要和浏览器版本最相近 <大版本一样, 小版本最相近> 模块使用: requests >>> pip install requests selenium >>> pip …

不是,到底有多少种图片懒加载方式?

一、也是我最开始了解到的 js方法&#xff0c;利用滚动事件&#xff0c;判断当时的图片位置是否在可视框内&#xff0c;然后进行渲染。 弊端&#xff1a;代码冗杂&#xff0c;你还要去监听页面的滚动事件&#xff0c;这本身就是一个不建议监听的事件&#xff0c;即便是我们做了…

【selenium学习】数据驱动测试

数据驱动在 unittest 中&#xff0c;使用读取数据文件来实现参数化可以吗&#xff1f;当然可以。这里以读取 CSV文件为例。创建一个 baidu_data.csv 文件&#xff0c;如图所示&#xff1a;文件第一列为测试用例名称&#xff0c;第二例为搜索的关键字。接下来创建 test_baidu_da…

百度生成式AI产品文心一言邀你体验AI创作新奇迹:百度CEO李彦宏详细透露三大产业将会带来机遇(文末附文心一言个人用户体验测试邀请码获取方法,亲测有效)

百度生成式AI产品文心一言邀你体验AI创作新奇迹中国版ChatGPT上线发布强大中文理解能力超强的数理推算能力智能文学创作、商业文案创作图片、视频智能生成中国生成式AI三大产业机会新型云计算公司行业模型精调公司应用服务提供商总结获取文心一言邀请码方法中国版ChatGPT上线发…

贪心算法的原理以及应用

文章目录0、概念0.1.定义0.2.特征0.3.步骤0.4.适用1、与动态规划的联系1.1.区别1.2.联系2、例子3、总结4、引用0、概念 0.1.定义 贪心算法&#xff08;greedy algorithm &#xff0c;又称贪婪算法&#xff09;是指&#xff0c;在对问题求解时&#xff0c;总是做出在当前看来是…

Java怎么实现几十万条数据插入(30万条数据插入MySQL仅需13秒)

本文主要讲述通过MyBatis、JDBC等做大数据量数据插入的案例和结果。 30万条数据插入插入数据库验证实体类、mapper和配置文件定义User实体mapper接口mapper.xml文件jdbc.propertiessqlMapConfig.xml不分批次直接梭哈循环逐条插入MyBatis实现插入30万条数据JDBC实现插入30万条数…

第十九天 Maven总结

目录 Maven 1. 前言 2. 概述 2.1 介绍 2.2 安装 3. IDEA集成Maven 3.1 集成Maven环境 3.2 创建Maven项目 3.3 Maven坐标详解 3.4 导入maven项目 4. 依赖管理 4.1 依赖配置 4.2 依赖传递 4.3 依赖范围 4.4 生命周期 4.5 插件 Maven 1. 前言 1). 什么是Maven? …

Linux实操之服务管理

文章目录一、服务(service)管理介绍:service管理指令查看服务名服务的运行级别(runlevel):CentOS7后运行级别说明chkconfig指令介绍一、服务(service)管理介绍: 服务(service)本质就是进程&#xff0c;但是是运行在后台的&#xff0c;通常都会监听某个端口&#xff0c;等待其它…

原力计划来了【协作共赢 成就未来】

catalogue&#x1f31f; 写在前面&#x1f31f; 新星计划持续上新&#x1f31f; 原力计划方向&#x1f31f; 原力计划拥抱优质&#x1f31f; AIGC&#x1f31f; 参加新星计划还是原力计划&#x1f31f; 创作成就未来&#x1f31f; 写在最后&#x1f31f; 写在前面 哈喽&#x…

依赖注入~

依赖注入之setter注入&#xff1a; 依赖注入是IOC具体的一种实现方式&#xff0c; 这是针对资源获取的方式角度来说的&#xff0c;之前我们是被动接受&#xff0c;现在IOC具体的实现叫做依赖注入&#xff0c;从代码的角度来说&#xff0c;原来创建对象的时候需要new&#xff0…

Phoenix基础命令_视图映射和表映射_数字存储问题---大数据之Hbase工作笔记0036

然后我们再来看看,用Phoenix来操作hbase,的基本用法 具体的其他的命令在官网都能找到,这里就说几个 https://phoenix.apache.org/language/index.html 首先是创建表,这里注意,默认表名给弄成大写的 这里的varchar对应的其实就是hbase中的string 然后这里的id表示行的rowkey 可…