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

目录

  • Day 11:多线程(9)
    • 生产者消费者模型
      • 1. 阻塞队列实现
    • 线程池
      • 1. 标准库线程池(ThreadPoolExecutor)
        • 1.1 corePoolSize & maximumPoolSize
        • 1.2 keepAliveTime & unit
        • 1.3 BlockingQueue<Runnable> workQueue
        • 1.4 ThreadFactory threadFactory
        • 1.5 RejectedExecutionHandler handler

Day 11:多线程(9)

生产者消费者模型

1. 阻塞队列实现

package thread;

class MyBlockingQueue {
    private String[] elems = null;
    //[head, tail)
    //head位置指向的是第一个元素,tail指向的是最后一个元素的下一个元素
    private volatile int head = 0;
    private volatile int tail = 0;
    private volatile int size = 0;

    public MyBlockingQueue(int capacity){
        elems = new String[capacity];
    }

    void put(String elem) throws InterruptedException {
        synchronized (this) {
            while (size >= elems.length){
                //队列满了,进行队列阻塞
                this.wait();
            }
            //把新的元素放到tail所在的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                //到达末尾,就回到开头
                tail = 0;
            }
            //更新size的值
            size++;


            //唤醒下面 take 阻塞的wait
            this.notify();
        }


    }

    String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                //队列空了,进行阻塞
                this.wait();
            }
            //取出 head 指向的元素
            String result = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }

            size--;
            //take 成功一个元素,就唤醒上面put中的wait操作
            this.notify();
            return result;
        }
    }
}

public class Demo31 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);

        Thread t1 = new Thread(() -> {
            try {
                int count = 1;
                while (true) {
                    queue.put(count + "");
                    System.out.println("生产" + count);
                    count++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                while (true){
                    String result = queue.take();
                    System.out.println("消费" + result);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });

        t1.start();
        t2.start();
    }
}
  • wait不仅仅会被notify/notifyAll唤醒,也可能被其他的操作唤醒,比如interrupt
  • 上述代码加锁后,采用while本质上是当wait被唤醒之后,再次确认条件是否满足,是否能往下执行
  • 如果采用if进行条件判断,而不是while,那么一旦wait被唤醒了,此时逻辑就会往下走,不管条件判断是否成立,当前这个代码,理论上不会出现这个情况,即便是被interrupt唤醒,也是方法直接结束
  • 但是按照文档的建议,使用while的方法,是更稳妥的做法,被唤醒之后,再次确认一下,确认一下条件是否成立
  • 最好的做法就是只要使用wait的时候,都搭配while判定条件的方式

线程池

并发编程,使用多线程就可以了,线程比进程更加轻量,在频繁创建和销毁的时候,更有优势,但是随着时代的发展,对于“频繁”有个新的定义,现有的要求下,频繁创建销毁线程,开销也变得越来越明显了

如何进行优化

  • 线程池
  • 协程(纤程)

后续高版本Java中引入的虚拟线程,本质上就是协程;Go语言的主打卖点,就是使用协程处理并发编程

为什么引入线程池/协程之后能够提升效率,最关键的要点是

  • 直接创建/销毁线程,是需要用户态+内核态配合完成的工作,直接调用api创建线程和销毁线程,这个过程需要内核完成,内核完成工作很多时候是不太可控
  • 线程池/协程,创建和销毁,只通过用户态即可,不需要内核态的配合,使用线程池,提前把线程都创建好,放到用户态代码中写的数据结构里面,后面使用的时候,随时从池子里去取,用完了放回池子里去,这个过程完全是用户态代码,不需要和内核进行交互

简单来讲,就是一次性从系统申请出10个线程,可以用10个变量表示,也可以用一个长度为10的元素的数组来表示,也可以用hash等表示

协程本质上也是纯用户态操作,规避内核操作,不是在内核里把线程提前创建好,而是用一个内核的线程来表示多个协程(纯用户态,进行协程之间的调度)

常量池、数据库连接池、线程池、进程池、内存池的思想都是一样的

1. 标准库线程池(ThreadPoolExecutor)

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
1.1 corePoolSize & maximumPoolSize
  • corePoolSize:核心线程数
  • maximumPoolSize:最大线程数

标准库的线程池把线程分为两类

(1)核心线程:corePoolSize

(2)非核心线程:maximumPoolSize = 核心线程数 + 非核心线程数

动态扩展

  • 一个线程池,刚被创建出来的时候,里面就包含核心线程数这么多的线程
  • 假设此时线程池里包含4个线程,线程池会提供一个submit方法,往里面添加任务,每个任务都是一个Runnable对象
  • 如果当前添加的任务比较少,4个线程就足以能够处理,就只有4个线程在工作了
  • 如果添加的任务比较多,4个线程处理不过来,有很多的任务在排队等待执行,这个时候线程就会自动创建出新的线程,来支撑更多的任务
  • 创建出来的线程总数,不能超过最大线程数
  • 过了一段时间之后,任务没那么多了,线程清闲了,部分线程就会被释放掉(回收了),回收只是把非核心线程回收掉,至少保证线程池中线程数目不少于核心线程数

既可以保证任务多的时候的效率,也能保证任务少的时候,系统开销小

实际开发中,线程数应该设置成多少合适

不仅和电脑配置有关,更重要的是,和程序的实际特点有关系

极端一点,可以将程序分成两个大类

  • CPU密集型程序:代码完成的逻辑,都是要通过CPU来完成的,此时线程数目,不应该超过CPU逻辑核心数,例如代码逻辑都是在进行算数运算、条件判定、循环判定和函数调用
  • IO密集型程序:代码大部分时间在等待IO,等待IO是不消耗CPU的,不参与调度,不仅仅是标准输入输出,也可能是sleep、操作硬盘、操作网络等,此时瓶颈不在CPU上,每个线程消耗CPU只有一点点,更多的是考虑其他方面,比如网络程序,要考虑网卡带宽的瓶颈

上述两类都太理想化了,实际开发中,一个程序中既包含CPU操作,也包含IO操作,介于CPU密集和IO密集两者之间

最终的答案是:根据实验的方式,找到一个合适的值来设置线程数,对程序进行性能测试,测试过程中,设定不同的线程池数值,最终根据实际程序的响应速度和系统开销综合权衡,找到一个你觉得最合适的值

1.2 keepAliveTime & unit
  • keepAliveTime:非核心线程,允许空闲的最大时间,在线程池不忙的时候回收掉
  • unit:上述时间的单位,比如秒、分钟、小时、毫秒等

例如,设定保留时间3s,3s之内,非核心线程没有任务执行了,此时就可以回收了

1.3 BlockingQueue workQueue

线程池的任务队列,线程池会提供submit方法,让其他线程把任务提交给线程池

线程池内部需要有一个队列这样的数据结构,把要执行的任务保存起来,后续线程池内部的工作线程,就会消费这个队列,从而来完成具体的任务执行

1.4 ThreadFactory threadFactory

标准库中提供的用来创建线程的工厂类,这个线程工厂主要是为了批量的给要创建的线程设置一些属性,在工厂方法中,把线程的属性提前初始化好了

工厂模式也是一种设计模式,主要解决的是:基于构造方法创建对象存在的问题

例子:创建一个类来表示一个点

class Point {
	public Point(double x, double y) {...} //笛卡尔坐标系
	public Point(double r, double a) {...} //极坐标系
}

上述代码会出现编译报错,这两个版本的构造方法不能构成重载,此时无法通过构造方法来表示不同的构造点的方式了,本质上因为构造方法名字是固定的,无法搞成不同的方法名字,要想提供不同的版本,就只能想办法在参数上做出区分

工厂模式的核心思路是:不再使用构造方法创建对象,而是给构造方法包装一层

package thread;

class Point {

}

class PointBuilder {
    public static Point makePointByXY(double x, double y) {
        Point p = new Point();
        p.setX(x);
        p.setY(y);
        return p;
    }

    public static Point makePointByRA(double r, double a) {
        Point p = new Point();
        p.setR(r);
        p.setA(a);
        return p;
    }
}

public class Demo33 {
    public static void main(String[] args) {
        Point p = PointBuilder.makePointByXY(10, 20);
    }
}
1.5 RejectedExecutionHandler handler

面试官问:线程池的参数都是什么意思,本质是在考拒绝策略

拒绝策略:是一个枚举类型,采用哪种拒绝策略

例如:如果当前任务队列满了,仍然要继续添加任务怎么办,下列四种拒绝策略

  • ThreadPoolExecutor.AbortPolicy:直接抛出异常
  • ThreadPoolExecutor.CallerRunsPolicy:谁负责添加任务,谁负责执行,线程池本身不管了
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃掉最老的任务,让新的任务去队列中排队
  • ThreadPoolExecutor.DiscardPolicy:丢弃最新的任务,按照原有的节奏来执行

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

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

相关文章

如何辨别:DNS污染or DNS劫持?

DNS劫持和DNS污染的情况在互联网中并不少见&#xff0c;到底是出现了DNS污染还是DNS劫持。什么是DNS污染&#xff1f;什么是DNS劫持&#xff1f;我们该如何辨别DNS污染和DNS劫持&#xff1f; DNS劫持&#xff1a; DNS 劫持是指恶意攻击者通过非法手段篡改了网络中的 DNS 服务…

HTML快速入门

HTML简介 HTML&#xff08;超文本标记语言&#xff09;是一种用于创建网页和Web应用程序的标记语言。它由一系列标签组成&#xff0c;每个标签通过尖括号来定义&#xff0c;并用于标记文本、图像、链接和其他内容。HTML标签描述了网页中的信息结构和布局&#xff0c;并定义了文…

[MySQL数据库] 索引与事务

1. 索引 1.1 概念 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针.可以对表中的一列或多列创建索引,并指定索引的类型&#xff0c;各类索引有各自的数据结构实现. 1.2 作用 数据库中的表、数据、索引之间的关系&#xff0c;类似于书架上的图书、书籍…

【Redis】面试题汇总

Redis什么是Redis、使用场景有哪些Redis 为什么这么快&#xff1f;Redis 数据类型及使用场景五种常见的 Redis 数据类型是怎么实现&#xff1f;Redis是单线程吗Redis 采用单线程为什么还这么快&#xff1f;Redis 如何实现数据不丢失&#xff1f;Redis 如何实现服务高可用&#…

【复习笔记】FreeRTOS(六) 队列操作

本文是FreeRTOS复习笔记的第六节&#xff0c;队列操作。 上一篇文章&#xff1a; 【复习笔记】FreeRTOS(五)时间片调度 文章目录 1.队列操作1.1.队列操作过程1.2.队列操作常用的API函数 二、实验设计三、测试例程四、实验效果 1.队列操作 队列是为了任务与任务、任务与中断之间…

极狐GitLab x LigaAI,AI 时代研发提效新范式

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 近日&#xff0c;极狐GitLab 和 LigaAI 宣布合作&#xff0c;双…

分布式锁设计

一、为什么需要分布式锁 1.1 单体项目同步实现 在单进程&#xff08;启动一个jvm&#xff09;的系统中&#xff0c;当存在多个线程可以同时改变某个变量&#xff08;可变共享变量&#xff09;时&#xff0c;就需要对变量或代码块做同步&#xff0c;使其在修改这种变量时能够线…

vue2中props属性设置一个对象或数组的默认值

在Vue.js中&#xff0c;如果您想要为一个props属性设置一个对象或数组的默认值&#xff0c;您应该使用一个函数来返回这个默认值。这是因为对象和数组是引用类型&#xff0c;直接将它们作为默认值可能会导致预设的默认值被所有实例共享&#xff0c;这不是我们想要的结果。 下面…

zabbix 自定义模板,邮件报警,代理服务器,自动发现与自动添加及snmp

目录 一. 自定义监控内容 1. 在客户端创建自定义 key 2. 在 web 页面创建自定义监控项模块 2.1 创建模板 2.2 创建应用集 2.3 创建监控项 2.4 创建触发器 2.5 创建图形 2.6 将主机与模板关联起来 登录测试 2.7 设置邮件报警 测试邮件报警 3. nginx 服务状况的检测…

Vue中SourceMap的使用方法详解

目录 一、概述 二、使用方法 三、生成SourceMap 四、优化 五、结语 一、概述 Vue.js是一套构建用户界面的渐进式框架&#xff0c;通过HTML模板或者直接写render函数可以快速开发单页应用。在开发过程中&#xff0c;很多时候我们需要调试代码&#xff0c;追踪错误。Vue官方…

Linux:调试器 - gdb

Linux&#xff1a;调试器 - gdb gbd基本概念gbd调试浏览断点运行变量 gbd基本概念 GDB (GNU Debugger) 是一个强大的命令行调试工具,用于调试各种编程语言(如C、C、Java、Python等)编写的程序。使用 gdb可以帮助开发人员更快地定位和修复程序中的缺陷,提高代码质量和开发效率。…

Python介绍(未完)

文章目录 Python 背景知识Python 是谁创造的&#xff1f;Python 可以用来干什么&#xff1f;Python 的优缺点 搭建 Python 环境安装 Python搭建 PyCharm 环境新工具到手&#xff0c;赶紧试试中文设置第一个Python程序 Python基础语法基础语法&#xff08;1&#xff09;常量和表…

Error : java 错误 : 不支持发行版本5 ( 完美解决)

解决方案 1. 原因 idea的默认配置JDK版本与当前项目所需版本不一样 方案一&#xff08;每一个项目可能都要配置一遍&#xff09; Ctrlshitalts 打开项目结构&#xff0c;设置项目所需的JDK版本&#xff0c;本项目需要JDK8 Modules的JDK版本为5&#xff0c;这时就会报Error …

最大公约数和最小公倍数(C语言)

一、N-S流程图&#xff1b; 二、运行结果&#xff1b; 三、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>//实现最大公约数函数&#xff1b; int max(int x, int y) {//初始化变量值&#xff1b;int judge 1;//运算&#xff1b;judge x %…

Ubuntu 23.10.1 nginx源码安装

注&#xff1a;以下所有命令均在root管理员模式下&#xff0c;若不是&#xff0c;请在所有命令前加sudo 1、安装依赖库 1.1、安装gcc g的依赖库 apt-get install build-essential apt-get install libtool1.2、安装pcre依赖库 apt-get update apt-get install libpcre3 lib…

剑指Offer题目笔记33(并查集)

面试题116&#xff1a; 解决方案&#xff1a; ​ 一个班级可以包含一个或多个朋友圈&#xff0c;对应的图中可能包含一个或多个子图&#xff0c;每个朋友圈对应一个子图。因此&#xff0c;这个问题可以转化为如何求图中子图的数目。图的搜索算法可以用来计算图中子图的数目。扫…

企业Linux特殊权限位/为什么会存在SUID?/企业环境测试(原理剖析)-4989字解析

企业高薪思维&#xff1a; 坚持很难&#xff0c;优秀的人才是少数&#xff0c;很重要 坚持不下去&#xff0c;问自己想要什么&#xff1f; 问问自己想要好的生活状态&#xff1f;问自己有背景吗&#xff1f;你学历是亮点吗&#xff1f;有钱没&#xff0c;你也就是一般家庭&…

selenium 下载文件取消安全下载的方法

问题描述 我要从一个网站上下载文件&#xff0c;谷歌浏览器总是自动阻止下载&#xff0c;并询问我是否保留。 可是&#xff0c;我想要的是不要询问&#xff0c;默认下载即可。 运行环境 OS: macOSselenium: 4.19.0python: 3.10.11Chrome: 124.0.6367.62selenium chromedrive…

工会排队模式:创新营销的双赢之道

工会排队模式全面解读 在当今数字化营销的大潮中&#xff0c;促销方式层出不穷&#xff0c;但能真正抓住消费者眼球并带来双方共赢的模式并不多见。而工会排队模式便是在这样的背景下崭露头角&#xff0c;它巧妙地融合了工会积分、奖金池与排队机制&#xff0c;为消费者与商家…

linux进阶篇:重定向和管道操作

Linux中的重定向和管道操作 llinux中的三种IO设备&#xff1a; 标准输入&#xff08;STDIN&#xff09;,文件描述符号为&#xff1a;0&#xff0c;默认从键盘获取输入 标准输出&#xff08;STDOUT&#xff09;,文件描述符号位&#xff1a;1&#xff0c;默认输出到显示终端 标准…