JavaEE-多线程初阶4

news2024/11/16 0:35:33

✏️作者:银河罐头
📋系列专栏:JavaEE

🌲“种一棵树最好的时间是十年前,其次是现在”

目录

  • 多线程案例
    • 阻塞队列
      • 阻塞队列是什么
      • 生产者消费者模型
      • 标准库中的阻塞队列
      • 阻塞队列实现
    • 定时器
      • 定时器是什么
      • 标准库中的定时器
      • 实现定时器
    • 线程池
      • 线程池是什么
      • 标准库中的线程池
      • 实现线程池

多线程案例

阻塞队列

阻塞队列是什么

阻塞队列,也是一个队列,之前数据结构中学过队列的特点是先进先出。

实际上还有一些特殊的队列,不一定非得遵守先进先出的规则。

比如优先级队列 PriorityQueue.

阻塞队列,也是先进先出的,但是带有特殊的功能:阻塞

如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里添加元素(队列不空)为止

如果队列满了,执行入队列操作,就会阻塞,阻塞到另一个线程从队列里取走元素(队列不满)为止

消息队列,也是特殊的队列,相当于是在阻塞队列的基础上加上了个"消息的类型",按照指定类别进行先进先出。

举个栗子🌰:医院里有个科室人很多,超声科,B超。B超能检查胃,肾,心脏,胎儿。

image-20230107202247226

此处谈到的"消息队列",仍然是一个"数据结构",

因为这个消息队列太香了,因此就有大佬把这样的数据结构,单独实现成了一个程序。这个程序可以通过网络的方式和其他程序进行通信。

此时,这个消息队列,就可以单独部署到一组服务器上(分布式),存储能力和转发能力都大大提高了。很多大型项目里,就可以看到这样的消息队列的身影。

此时,消息队列,就已经成了一个可以和mysql , redis相提并论的一个重要组件了。“中间件”

要想认识清楚消息队列,还是得认识清楚"阻塞队列"。

为啥消息队列这么香?和阻塞队列阻塞特性关系很大。

基于这样的特性,可以实现"生产者消费者模型"

生产者消费者模型

举个栗子🌰:过年有个环节就是年夜饭家里人一起包饺子。

包饺子的步骤:擀饺子皮 + 包饺子

两种典型的包法:

1.每个人都进行擀饺子皮 + 包饺子 的操作。

(这种情况大家会竞争唯一的擀面杖,就会产生阻塞等待影响效率)

2.一个人专门负责擀饺子皮,剩下其他人负责包饺子。每擀好一个饺子皮,就放到盖帘上,其他人从盖帘上取一个皮来包饺子。

很明显,第二种包法更好,这种方式就称为 生产者消费者 模型,擀饺子皮的人是 生产者,剩下其他包饺子的人是 消费者,盖帘就是阻塞队列。如果生产者擀饺子皮擀太慢了(盖帘空了),包饺子的人就得等;如果擀太快了,盖帘满了,生产者就得等(包饺子的人把饺子皮取走)。

生产者消费者 模型,能给我们的程序带来两个非常重要的好处。

  • 1.实现了发送方和接收方之间的"解耦"

开发中典型的场景:服务器之间的相互调用

image-20230107205017722

此时A 把请求转发给 B 处理,B 处理完了把结果反馈给 A,此时就可以视为 “A 调用了 B”。上述场景中,A 和 B 之间的耦合是比较高的。A 调用 B ,A 务必要知道 B 的存在,如果 B 挂了就很容易引起 A 的 bug !

另外,如果要是再加一个 C 服务器,此时就需要对 A 修改不少代码,因此就需要针对 A 重新修改代码,重新测试,重新发布,重新部署…这样就很麻烦

针对上述场景,使用 生产者消费者 模型就可以有效降低耦合

image-20230107211732276

此时 A 和 B 之间的耦合就降低很多了。

A 是不知道 B 的,A 只知道队列( A 的代码中没有关于 B 的);

B 也是不知道 A 的,B 只知道队列( B 的代码中没有关于 A 的)。

如果 B 挂了对 A 没有任何影响,因为队列还是正常的,A 仍然可以给队列里插入元素,如果队列满了就先阻塞;如果 A 挂了对 B 没有任何影响,因为队列还是正常的,B 仍然可以从队列里取出元素,如果队列空了就先阻塞。

总之 A , B 任何一方挂了对 对方都不会造成影响

新增一个 C 作为消费者,对于 A 来说仍然是无感知的

  • 2.“削峰填谷”,保证系统的稳定性。

举个栗子🌰:三峡大坝,起到的效果就是 “削峰填谷”。

image-20230107212818999

如果上游水多了,三峡大坝关闸蓄水,此时就相当于三峡大坝承担了上游的冲击,保护了下游(削峰);

如果上游水少了,三峡大坝开闸放水,有效保证下游的用水情况,避免出现干旱灾害(填谷)。

服务器开发,也和上述模型非常类似。

咱们的上游就是用户发送的请求,下游就是一些执行具体业务的服务器。

用户发多少请求时不可控的,有时候请求多,有时候请求少。

举个栗子🌰:“热搜”

突然某个瞬间,很多用户都发送请求,此时如果没有充分的准备(使用生产者消费者模型是一个有效的手段),服务器一下扛不住就挂了。

标准库中的阻塞队列

image-20230109182123206

Queue 提供的方法有:1.入队列 offer 2.出队列 poll 3.取队首元素 peek

阻塞队列的主要方法是 2 个:1.入队列 put 2. 出队列 take (这 2 个方法都是带有阻塞功能的)

package Thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//阻塞队列的使用
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
        blockingQueue.put("hello");
        String res = blockingQueue.take();
        System.out.println(res);
    }
}
//输出:
hello
package Thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

//阻塞队列的使用
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
        blockingQueue.put("hello");
        String res = blockingQueue.take();
        System.out.println(res);
        res = blockingQueue.take();//如果队列空了再取元素就会阻塞
        System.out.println(res);
    }
}
//输出:
hello
(程序没有结束)
package Thread;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo{
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
        Thread customer = new Thread(()->{
            while(true){
                try {
                    Integer result = blockingQueue.take();
                    System.out.println("消费元素: " + result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

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

阻塞队列实现

要想实现一个阻塞队列,首先要实现一个普通的队列。

这个普通的队列:1.基于链表实现(头删尾插) 2.基于数组实现 , 环形队列:[head,rear)

普通队列加上阻塞功能。

阻塞功能意味着队列要在多线程环境下使用。

要保证线程安全。所以要加上锁,使用 synchronized。

//基于数组循环队列自己写的阻塞队列
package Thread;
class MyBlockingQueue{
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    //入队列
    public void put(int val) throws InterruptedException {
        //判满
        synchronized (this) {
            while(size == items.length){//可能 wait()唤醒之后队列还是满的
                //return;
                this.wait();//队列满了,此时要发生阻塞
            }
            items[tail] = val;
            tail++;
            //1)
            // tail = tail % items.length;//进行除法操作,更慢的操作
            //2)
            if(tail >= items.length){
                tail = 0;
            }
            //2)比 1)可读性更好,代码效率比 % 高。判断 + 赋值,虽然是 2 个操作,2 个操作都是高效操作
            size++;
            this.notify();//唤醒 take里的 wait
        }
    }
    //出队列
    public Integer take() throws InterruptedException {
        //判空
        int result = 0;
        synchronized (this) {
            while(size == 0){
                //return null;
                this.wait();//队列空了,此时要发生阻塞
            }
            result = items[head];
            head++;
            if(head >= items.length){
                head = 0;
            }
            size--;
            this.notify();//唤醒 put 里的 wait
        }
        return result;
    }
}
public class ThreadDemo{
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();
        Thread customer = new Thread(()->{
            while(true){
                try {
                    int result = queue.take();
                    System.out.println("消费元素: " + result);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
        Thread producer = new Thread(()->{
            int count = 0;
            while(true){
                try {
                    System.out.println("生产元素: " + count);
                    queue.put(count);
                    count++;

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

while(size == 0){
//return null;
this.wait();//队列空了,此时要发生阻塞
}

image-20230109220158410

标准库建议是这么写的

定时器

定时器是什么

闹钟:1.指定特定时间提醒 2.指定特定时间段之后提醒(定时器)

这里的定时器,不是提醒,而是执行一个准备好的方法/代码

这个是开发中常用的一个组件,尤其是网络编程的时候。网络有时候可能不太顺畅,很容易出现"卡了"“连不上"的情况,就可以使用定时器来"及时止损”。

标准库中的定时器

package Thread;

import java.util.Timer;
import java.util.TimerTask;
public class ThreadDemo{
    public static void main(String[] args) {
        System.out.println("程序启动");
        Timer timer = new Timer();//这个Timer类就是标准库的定时器
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("运行定时器任务");
            }
        },3000);
    }
}

image-20230109223657827

timer.schedule() 这个方法的效果是给定时器注册一个任务,任务不会立即执行,而是在指定时间执行。

实现定时器

自己来实现一个定时器:
1.让被注册的任务,在指定时间去执行

单独在定时器内部创建一个线程,让这个线程周期性的扫描,判断任务是否到时间了,到时间了就执行,没到时间的就继续等。

2.一个定时器可以执行 N 个任务。N个任务会按照指定的时间,按顺序执行。

这 N 个任务需要用数据结构来保存。用优先级队列这个数据结构。

每个任务都带有自己的时间,多久执行,时间越靠前的越先执行。

时间小的优先级高,队首元素就是最先执行的任务,扫描线程只需要扫一下队首元素就可以(不必遍历整个队列)

总结:1.有一个扫描线程,判断到时间执行任务 2. 用优先级队列来保存所有被注册的任务

此处的优先级队列会在多线程环境下使用。

调用schedule 是一个线程,扫描是另一个线程。

多线程环境下,肯定要关注线程安全问题。

标准库提供了 PriorityBlockingQueue , 它本身是线程安全的。

package Thread;

import java.util.concurrent.PriorityBlockingQueue;

class MyTask{
    //要执行的任务内容
    private Runnable runnable;
    //任务在啥时候执行,用 ms 时间戳来表示
    private long time;
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的时间
    public long getTime() {
        return time;
    }
    //执行任务
    public void run(){
        runnable.run();
    }
}
class MyTimer{
    //扫描线程
    private Thread t = null;
    //有一个阻塞队列来保存任务
    private PriorityBlockingQueue<MyTask>  queue = new PriorityBlockingQueue<>();
    public MyTimer(){
        t = new Thread(()->{
            while(true){
                //取出队首元素,看看是否到时间了,
                //到时间了就执行任务
                //没到时间就放回队列中
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if(curTime < myTask.getTime()){
                        //没到时间
                        queue.put(myTask);//会触发优先级调整,myTask又回到队首,下次取出的还是这个任务

                    }else{
                        //到时间了
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    //指定 2 个参数
    //1.任务内容
    //2.任务执行的时间
    public void schedule(Runnable runnable,long after){
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
    }
}

public class ThreadDemo{
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }
}

image-20230111152253616

让 MyTask 类实现 Comparable 接口或使用 Comparator 写个比较器

@Override
    public int compareTo(MyTask o) {
        //要么是 this.time - o.time,要么是 o.time - this.time.
        //到底是哪个,运行看下就知道了
        return (int)(this.time - o.time);
    }

image-20230111155328964

还有一个问题是:扫描线程里 while(true),取出一个任务时间没到又塞回队列里。一直重复,这种操作成为"忙等"。

等,但是并没有闲着,按理来说,等待是要释放CPU资源的,让CPU去干别的事。但是忙等既进行了等待,又占用CPU资源

举个栗子🌰:比如你定了8:00的闹钟,但是你7:30就醒了,睡一会儿又醒来看还是7:30,睡一会儿又醒来看还是7:30…(是一直在等,但是也没闲着)

像忙等这种情况,需要辩证性的去看待,当前这种情景下 忙等 是不好的, 但是有的情况下忙等是一个好的选择。

针对以上代码,不要再"忙等"了,进行阻塞式等待。

此处的等待是多久?假设当前是1:00,队首元素是2:00,那么等待时间就是1个小时。

此处的等待时间看似明确,其实也并不明确。因为随时都可能有新任务到来(随时可能有线程调用schedule()添加新任务),新任务时间可能更早。

所以这里使用 wait()更合适,更方便随时唤醒,如果有新任务来了,就notify()一下,重新确认队首元素,重新计算需要等待的时间。

带超时时间的 wait() ,可以保证1.如果有新任务来了,随时唤醒。2.如果没有新任务,就等到旧任务的最早时间。

程序里的计时操作,本身就难以做到非常精确,(操作系统调度线程有时间开销的),存在 ms 级别的误差都很正常,也不影响日常使用。如果你的使用场景,就是对时间误差非常敏感,(发射导弹,发射卫星),此时就不能用 windows,Linux 这样的操作系统了,而应该使用像 vxworks 这样的实时操作系统(线程调度是开销极快,可控的,可以保证误差是在要求范围内的)

代码写到这里,还有一个问题,是和线程安全/随机调度相关的。

image-20230111164807352

image-20230111164820738

//多线程
//有 2个线程
//1.线程 t
//2.主线程 main

image-20230111165914202

此处只需要把锁的范围放大,放大之后就可以保证执行 notify() 的时候 wait() 已经执行完了。

完整代码:

package Thread;

import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask> {
    //要执行的任务内容
    private Runnable runnable;
    //任务在啥时候执行,用 ms 时间戳来表示
    private long time;
    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的时间
    public long getTime() {
        return time;
    }
    //执行任务
    public void run(){
        runnable.run();
    }


    @Override
    public int compareTo(MyTask o) {
        //要么是 this.time - o.time,要么是 o.time - this.time.
        //到底是哪个,运行看下就知道了
        return (int)(this.time - o.time);
    }
}
class MyTimer{
    //扫描线程
    private Thread t = null;
    //有一个阻塞队列来保存任务
    private PriorityBlockingQueue<MyTask>  queue = new PriorityBlockingQueue<>();
    public MyTimer(){
        t = new Thread(()->{
            while(true){
                //取出队首元素,看看是否到时间了,
                //到时间了就执行任务
                //没到时间就放回队列中
                try {
                    synchronized (this) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (curTime < myTask.getTime()) {
                            //没到时间
                            queue.put(myTask);
                            this.wait(myTask.getTime() - curTime);

                        } else {
                            //到时间了
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    //指定 2 个参数
    //1.任务内容
    //2.任务执行的时间
    public void schedule(Runnable runnable,long after){
        MyTask myTask = new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(myTask);
        synchronized (this){
            this.notify();
        }
    }
}

public class ThreadDemo{
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务1");
            }
        },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }
}

线程池

线程池是什么

因为进程来实现并发编程太"重"了,此时引入了线程,线程的创建,销毁和调度都比进程更高效。此时使用多线程在很多时候就可以代替进程来实现并发编程了。

随着并发程度的提高,随着我们对性能要求标准的提高,发现线程的创建也没有那么轻量。

当我们需要频繁的创建销毁线程的时候,就发现开销还是很大的。

想要进一度的提高这里的效率,有 2 种办法:

1.搞一个"轻量型线程"=>协程/纤程(很遗憾,这个东西目前还没有加入到 Java 标准库).

2.使用线程池,来降低创建/销毁线程的开销。

说到池,可以联想到字符串常量池,数据库连接池…

事先把需要用的线程创建好,放到"池"中,后面需要使用的时候,直接从池里获取,用完了就还给池。

从池子里取和放回池子这比创建/销毁更高效。

创建/销毁线程是由操作系统内核完成的,从池子里取和放回池子这是我们自己用户代码就能实现的,不必交给内核操作

什么是操作系统内核?

image-20230119184039998

在银行大厅里,用户都是自主的。就像程序中的"用户态","用户态"执行的是程序员自己写的代码,这里想干啥,怎么干,都是程序员代码自主决定的。

有些操作需要在银行柜台后完成,你不能进入柜台,需要通过银行的工作人员通过他们来间接完成。就像程序中的"内核态",内核态进行的操作都是在操作系统内核中完成的。内核会给程序提供一些api,称为系统调用,驱使内核完成一些工作。

系统调用里面的内容是直接和内核的代码相关的。这一部分工作不受程序员自主控制,都是内核自行完成的。

相比于内核来说,用户态,程序执行的行为是可控的。想要做某个工作就会非常干净利落的完成。(比如从池子里取/还给池子)。

如果要是通过内核从系统这里创建个线程,就需要通过系统调用,让内核来执行了。此时你不清楚内核身上背负多少任务(内核不是只给你一个应用程序服务的,给所有的程序都要提供服务)

因此,当使用系统调用,执行内核代码的时候,无法确定内核都要做哪些工作,整体过程是"不可控"的。

标准库中的线程池

package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);//这里的 new 是方法名的一部分,不是 new 关键字。
       
        //创建了一个线程池,池子里线程数目固定是 10 个
    }
}

这个操作,使用某个类的某个静态方法直接构造出一个对象来(相当于是把 new 操作给隐藏到静态方法后面了)

像这样的方法,就称为"工厂方法",提供这个工厂方法的类就称为"工厂类"。此处这个代码就使用了"工厂模式"这种设计模式

在当前校招阶段,要研究的设计模式,单例和工厂

工厂模式:使用普通方法来代替构造方法,创建对象。

(为啥要代替?构造方法有坑,坑就体现在只构造一种对象就好办,如果要构造多种不同情况下的对象就难办了)

举个栗子🌰:

想要表示二维平面上一个点,有 2 种办法:平面直角坐标系或极坐标系

class Point{
    public Point(double x,double y){

    }
    public Point(double r,double a){

    }
}

但是,很明显这段代码有问题,这 2 个构造方法不符合重载的要求(方法名相同,参数列表不同)

为了解决这个问题就可以使用工厂模式

class PointFactory{
    public static Point makePointByXY(double x,double y){

    }
    public static Point makePointByRA(double r,double a){

    }
}
public class ThreadDemo{
    public static void main(String[] args) {
        Point p = PointFactory.makePointByXY(10,20);
    }
}

普通方法,方法名字没有限制,因此有多种方式构造,就可以直接使用不同的方法名即可,此时方法的参数是否要区分已经不重要了。

像工厂模式,对 Python 来说没什么价值,Python 构造方法不像 C++/Java 这么坑,可以直接在构造方法中通过替他手段来做出不同版本的区分。

不同语言,语法规则不一样,因此在不同语言上,能够使用的设计模式可能会不同。有的设计模式已经融合在语言语法内部了。

日常谈到的设计模式,主要是基于 C++/Java/C# 这样的语言来展开的,这里说的设计模式不一定适合其他语言。

 ExecutorService pool = Executors.newFixedThreadPool(10);

线程池提供了一个重要的方法,submit,可以给线程池提交若干个任务

image-20230120201753253

运行程序发现,main线程结束了,但是整个进程没有结束。线程池中的线程都是前台线程,此时会阻止进程结束。

前面定时器 Timer 也是同理

ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0;i < 1000;i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello " + n);
                }
            });
        }

当前是往线程池里放了 1000 个任务,1000 个任务就是这 10 个线程平均分配一下,差不多是一个分配 100 个,这里不是严格的平均,有的多几个,有的少几个都正常。(每个线程执行完一个任务后会立即取下一个任务,由于每个任务执行时间差不多,所以每个线程执行的任务数量也差不多)

进一步的可以认为,这 1000 个任务,就在一个队列里排队呢,这 10 个线程就一次来取队列里的任务,取一个就执行一个,执行完了之后就执行下一个,这个操作很类似做核算…

  • 变量捕获

Java当中的匿名内部类, Lambda 表达式中,会存在变量捕获

run方法属于Runnable,这个方法的执行时机,不是立刻马上,而是在未来的某个节点,后续在线程池的队列中,排到它了就让对应的线程去执行。

i 是主线程里的局部变量,(在主线程的栈上),随着主线程的代码块执行结束就销毁了,很可能主线程的for执行完了,而当前 run 的任务在线程池里还没排到呢,此时 i 就已经要销毁了。

为了避免作用域的差异,导致后序执行 run 的时候 i 已经销毁,于是就有了变量捕获,也就是让run方法把主线程的 i 往当前 run 的栈上拷贝一份(在定义 run 的时候,偷偷把 i 的值记住,后续执行 run 的时候创建一个也叫 i 的局部变量,并把这个值赋值过去)

在 Java 中,对于变量捕获,做了一些额外的要求。在 JDK 1.8 之前,要求变量捕获只能捕获 final 修饰的变量。1.8 之后要求这个变量要么是被final修饰,如果不是被final修饰的你要保证在使用之前,没有修改。

此处的 i 是有修改的,不能捕获,而 n 是没有修改的,虽然没有被 final 修饰但是也能捕获。

image-20230120213005131

上述这些线程池,本质上都是通过包装 ThreadPoolExecutor 来实现出来的

ThreadPoolExecutor 这个线程池用起来更麻烦一点(提供的功能更强大),所以才提供了工厂类,让我们用着更简单。

  • java.util.concurrent

这个包里放的很多类都是和并发编程(多线程编程)密切相关的,这个包也简称为 juc

image-20230120214555103

corePoolSize 核心线程数

maximumPoolSize 最大线程数

ThreadPoolExecutor 相当于把里面的线程分成2类:①正式员工(核心线程) ②临时工/实习生

这2个之和是最大线程数。

允许正式员工摸鱼,不允许实习生摸鱼,如果实习生摸鱼太久就会被开除(销毁)

如果任务多,就需要更多的人手(更多的线程),但是一个程序任务不是一直都多,有时候多有时候少。

如果任务少的情况下线程很多就不合适了,需要对现有的线程进行淘汰,整体策略是正式员工保底,实习生动态调节。

实际开发的时候,线程池的线程数,设成多少合适?网上的资料有说:N(CPU 的核数),N + 1, 1.5N ,2N…这些说法都不准确,如果面试中遇到这样的问题,只要你回答出了具体的数字一定就是错的。

不同程序特点不同,此时要设置的线程数也是不同的,

考虑 2 个极端情况:
1.CPU密集型:每个线程要执行的任务都是狂转CPU(进行一系列算术运算),此时线程池的线程数最多也不该超过CPU核数(此时你设置的再大也没用,CPU密集型任务,要一直占用CPU,搞那么多线程,CPU不够了)

2.IO密集型:每个线程干的工作就是等待IO(读写硬盘,读写网卡,等待用户输入…),不吃CPU,此时这样的线程处于阻塞状态,不参与CPU调度…此时多搞一些线程都无所谓,不再受制于CPU核数了,理论上来说你线程数设置成无限大都可以(实际上肯定是不行的).

然而实际开发中,并没有程序符合这两种理想模型…真实的程序,往往一部分要吃CPU,一部分要等待IO,具体这个程序几成工作量是吃 CPU的,几成是等待IO的,不确定。

实践中确认线程的数量也非常简单,通过测试/实验的方式。

现代的CPU,是一个物理核心上可以有多个逻辑核心的, 8核16线程,8个物理核心,每个物理核心上有2个逻辑核心,每个逻辑核心同一时刻只能处理一个线程。

long keepAliveTime 是线程池中空闲线程等待工作的超时时间(实习生可以摸鱼的最大时间)

TimeUnit unit 时间单位(s,ms,分钟…)

BlockingQueue<[Runnable> workQueue 线程池的任务队列

此处使用阻塞队列,每个工作线程都是在不停尝试 take ,如果有任务就 take 成功,没有任务就阻塞等待

ThreadFactory threadFactory 用于创建线程的,线程池是需要创建线程的。

RejectedExecutionHandler handler 描述了线程池的"拒绝策略",也是一个特殊的对象,描述了如果线程池任务队列满了,如果再继续添加任务会有什么样的行为。

标准库提供的4个拒绝策略

image-20230121111607249

举个栗子🌰:假设张三最近很忙,快要考试了,张三在准备复习几门课程,这时候室友李四让张三帮他写个作业。

第1种拒绝策略:张三哇的一声哭出来,摆烂了,考试也不复习了,作业也不帮李四写了,(抛出异常)

第2种拒绝策略:张三时间都排满了,没有多的时间帮李四了,让李四自己的作业自己写,继续复习考试

第3种拒绝策略:张三舍己为人,不复习了,帮李四写作业

第4种拒绝策略:张三拒绝帮李四写作业

实现线程池

来实现一个固定数量线程的线程池

一个线程池,至少有2部分:

1.阻塞队列,用来保存任务

2.若干个工作线程

package thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    //此处不涉及到"时间",此处只有任务,直接使用 Runnable 即可
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // n 表示线程的数量
    public MyThreadPool(int n){
        //在这里创建出线程
        for (int i = 0;i < n;i++){
            Thread t = new Thread(()->{
                while(true){//循环取任务
                    try {
                        Runnable runnable = queue.take();//取任务
                        runnable.run();//执行任务
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
    //注册任务给线程池
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadDemo{
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for(int i = 0;i < 1000;i++){
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello " + n);
                }
            });
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(第107篇)C规范编辑笔记(十三)

往期文章&#xff1a; C规范编辑笔记(一) C规范编辑笔记(二) C规范编辑笔记(三) C规范编辑笔记(四) C规范编辑笔记(五) C规范编辑笔记(六) C规范编辑笔记(七) C规范编辑笔记(八) C规范编辑笔记(九) C规则编辑笔记(十) C规范编辑笔记(十一) C规范编辑笔记(十二) 正文&#xff…

行人属性识别研究综述(二)

文章目录6 PAR&#xff08;行人属性识别&#xff09;算法综述6.1全局基于图像的模型6.1.1 ACN (iccvw-2015)6.1.2 DeepSAR and DeepMAR (ACPR-2015) [6]6.1.3 MTCNN (TMM-2015) [7]6.2 基于部件的模型6.2.1 Poselets (ICCV-2011)6.2.2 rad (iccv-2013)6.2.3 PANDA (cvp -2014) …

Java-IO知识详解(一)

分类分类&#xff08;传输&#xff0c;操作&#xff09;IO理解分类 - 从传输方式上字节流字符流字节流和字符流的区别IO理解分类 - 从数据操作上文件(file)数组([])管道操作基本数据类型缓冲操作打印对象序列化反序列化转换装饰者模式分类&#xff08;传输&#xff0c;操作&…

迈百瑞冲刺创业板上市:关联收入占比较高,房健民为加拿大籍

撰稿|汤汤 来源|贝多财经 近日&#xff0c;烟台迈百瑞国际生物医药股份有限公司&#xff08;下称”迈百瑞“&#xff09;在深圳证券交易所提交更新后的招股书&#xff08;申报稿&#xff09;。据贝多财经了解&#xff0c;迈百瑞于2022年9月在递交IPO申请材料&#xff0c;准备…

指定不同版本的pcl

18.04里面安装了两个版本的pcl&#xff0c;一个是安装ros的时候安装的pcl1.8&#xff0c;另一个是安装的源码pcl1.12版本。一直相安无事&#xff0c;今天在我编译lego-loam的时候&#xff0c;突然就冲突了。卡了我两个小时&#xff0c;到处找原因&#xff0c;网上基本上没有相似…

RSD高分卫星数据处理能力提升——日正射处理数千景高分数据集

李国春 通常认为&#xff0c;能够单日处理几百景高分辨率对地观测卫星数据的系统就已经是非常优秀的卫星数据处理系统了。RSD此次优化将其处理能力提升超过了一个数量级&#xff0c;达到了单日正射处理数千景高分辨率卫星数据集的水平。 不仅如此&#xff0c;RSD达到如此高的…

SpringBoot+Vue项目(学生信息管理系统)搭建运行

项目地址&#xff1a;学生信息管理系统 前端部分&#xff08;Vue&#xff09; 首先以管理员身份运行终端 不然运行命令时有些会报错 1.首先下载node.js 2.打开并安装node.js 3.安装完成&#xff0c;打开控制台&#xff0c;输入node -v查看是否安装完成&#xff0c;如果显示…

MongoDB学习笔记【part2】数据库、文档、集合与常用命令

一、MongoDB 概念 Mongo 与 SQL 的术语区别如下&#xff1a; SQL术语/概念MongoDB术语/概念解释/说明databasedatabase数据库tablecollection数据表 – 集合rowdocument记录 – 文档columnfield字段 – 域indexindex索引table joins表连接&#xff0c;MongoDB不支持primary k…

分享121个PHP源码,总有一款适合您

PHP源码 分享121个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 121个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1--fhiwI0gwB1a2ouivPw7g?pwdd61x 提取码&#x…

监控指标解读和JVM 分析调优

1、中间件指标  当前正在运行的线程数不能超过设定的最大值。一般情况下系统性能较好的情况下&#xff0c;线 程数最小值设置 50 和最大值设置 200 比较合适。  当前运行的 JDBC 连接数不能超过设定的最大值。一般情况下系统性能较好的情况下&#xff0c; JDBC 最小值设置 …

Transformers学习笔记4

Tokenizernlp任务的输入都是raw text&#xff0c;model的输入需要是inputs id&#xff0c;所以tokenzier将句子转换成inputs id&#xff0c;怎么转换呢&#xff0c;有3种方式&#xff1a;word-basedsplit the text&#xff1a;按照空格来区分按照标点来区分我们会得到一个非常大…

Element-UI的dialog对话组件内的tinymce弹窗被遮挡的解决办法及其它相关注意事项

问题一&#xff1a;tinymce的弹窗被遮挡 问题截图 解决办法 修改层级 注意要写在 <style></style> 中&#xff0c;我当时没注意&#xff0c;写在了 <style scoped></style> 中&#xff0c;死活没反应。 <style> /* 在el-dialog中tinymce z-ind…

C进阶_指针和数组试题解析

农历新年即将到来&#xff0c;我在这里给大家拜年了&#xff01;祝大家新的一年心想事成&#xff0c;皆得所愿。新的一年&#xff0c;新的征程&#xff0c;愿各位继续怀揣梦想和远方&#xff0c;奔赴每一场山海。我们一起砥砺前行&#xff0c;“卯定乾坤”&#xff01; 老老少…

Spring Boot操作数据库学习之整合JDBC

文章目录一 Spring Data简介二 Spring Boot集成JDBC2.1 项目创建步骤2.2 测试的SQL脚本2.3 编写yaml配置文件连接数据库2.4 IDEA连接数据库步骤【非必要的步骤】2.5 测试2.6 运行结果2.7 对运行结果的探究2.8 遇到的错误2.8.1 问题1&#xff1a;jdk编译版本2.8.2 解决方法2.8.3…

低成本搭建一台 Unraid 家庭存储服务器:中篇

虎年最后一篇文章&#xff0c;接着上一篇内容&#xff0c;聊聊如何提升硬件的易用性问题。 写在前面 如果你的诉求非常简单、明确&#xff0c;不需要界面&#xff0c;上一篇内容中的 Ubuntu Server 应该已经能够完成你的诉求了。 但是&#xff0c;如果你和我一样&#xff0c…

【Java数据结构与算法】day4-稀疏数组和队列(环形队列)

✅作者简介&#xff1a;热爱Java后端开发的一名学习者&#xff0c;大家可以跟我一起讨论各种问题喔。 &#x1f34e;个人主页&#xff1a;Hhzzy99 &#x1f34a;个人信条&#xff1a;坚持就是胜利&#xff01; &#x1f49e;当前专栏&#xff1a;Java数据结构与算法 &#x1f9…

第五届字节跳动青训营 前端进阶学习笔记(七)HTTP协议入门

文章目录前言HTTP协议概述1.输入网址到页面加载完成中间发生了什么2.HTTP协议3.HTTP协议的发展历程4.HTTP报文结构&#xff08;1&#xff09;HTTP请求报文&#xff08;2&#xff09;HTTP响应报文5.请求方法&#xff08;1&#xff09;安全的方法&#xff08;2&#xff09;幂等6.…

表单标签的学习

表单标签的学习 textarea textarea标签来表示多行文本框&#xff0c;又叫做文本域。与其它 标签不同&#xff0c; textarea标签是单闭合标签&#xff0c;它包含起始标签和结束标签&#xff0c;文本内容需要写在两个标签中间。 input input type“text” 表示文本框 &#xff…

CSS入门学习笔记+案例

目录 一、 CSS的基础 1、快速了解 2、CSS应用方式 ①在标签上 ②在head标签中写style标签 ③写到文件中 二、CSS的选择器 1、ID选择器 2、类选择器 3、标签选择器 4、属性选择器 5、后代选择器 三、样式覆盖 四、CSS的样式 1、高度和宽度 2、块级和行内标签 3、字体…

七大排序---详细介绍

插入排序从第二个数&#xff0c;往前面进行插入&#xff0c;默认第一个数字有序&#xff0c;插入第二个&#xff0c;则前两个都有序了&#xff0c;一个一个往后选择数字&#xff0c;不断向前进行插入直接插入排序时间复杂度&#xff1a;最好情况&#xff1a;全部有序&#xff0…