zl程序教程

您现在的位置是:首页 >  其他

当前栏目

多线程案例—— 线程池

2023-04-18 16:48:59 时间

目录

一、线程池

1、 减小开销的内部原因

2、 各个参数的含义

3、 ***线程池的工作流程 *** 

4、 ***如何设置参数 *** 

5、 使用标准库中的线程池—— Executors 类

6、 自己实现一个 线程池


一、线程池

   线程池就是提前创建好了一批线程,放到一个池子中,当有任务来的时候,就从池子中去取出一个线程(就不用从系统这边申请了),去执行这个任务,执行完后又放回到池子中。

  •    进程比较 “重”,频繁的创建和销毁开销很大,因此有了 进程池线程
  •    线程是 “轻” 了,但是更频繁的创建销毁还是会有很大的开销。因此有了 线程池 和 协程

   使用线程池的话,就没有频繁的创建销毁了,速度就快了 。

1、 减小开销的内部原因

   主要原因就是 用户态 和 内核态 的速度。

    

    上图是计算机的大致层次结构。我们平常写的代码就是在最上层 应用程序 这层来运行的,而这里的代码都被称为: “用户态” 运行的代码。 但是有写代码需要调用操作系统的 API,从而会在内核中执行。(例如调用 System.out.println,本质上要经过 write 系统调用,进入到内核中,内核会执行很多逻辑,最后控制显示器输出字符)在内核中运行的代码,就被称为: “内核态” 运行的代码

   创建线程,本身就需要内核的支持。(本质是在内核中创一个 PCB ,加到链表里)因此调用的 Thread.start 也是要进入内核态来运行的。

   而把创建好的线程放在池子里,因为这个池子就是用用户态实现的,因此把线程放到池子 / 从池子中取出线程,都是用纯粹的用户态代码实现的,不涉及内核态。

   我们认为,纯用户态的操作,效率要比经过内核态处理的操作,效率更高。因为当代码进入内核态后,由于内核态要做的操作有很多,我们不知道什么时候才能执行我们这个代码,因此具有不可控性。所以说纯用户态的操作效率更高。

2、 各个参数的含义

  •    标准库的线程池: ThreadPoolExecuter

其内部的参数:

  •  int corePoolSize :核心线程数 (正式员工的数量)
  •  int maximumPoolSize :最大线程数(正式员工 + 临时员工 的数量)
  • long keepAliveTime :线程的空闲时间(允许临时员工摸鱼的时间)
  • TimeUnit unit :时间单位(s、ms、us....)
  • BlockingQueue<Runnable> workQueue :任务队列(线程池提供一个 submit 方法,让我们把任务注册到线程池中,加入到这个任务队列)
  • ThreadFactory threadFactory :线程工厂(线程是怎么创建出来的,一般使用默认的)
  • RejectedExecutionHandler handler :拒绝策略(当任务队列满了的时候怎么办?直接丢弃最老的任务 / 阻塞等待 / 直接忽略最新的任务 ....)

    类似在一个公司中,既有正式员工也有临时员工。正式员工不能随便开除,而临时员工有任务的时候就招进来,没有任务了就解雇。(任务多了就创建一些临时线程,任务执行完了就销毁)但是临时工也不能一干完活就直接解雇,公司要观察这一段时间的工作量,发现最近一段时间没有较多任务了,才会裁掉。(线程的空闲时间)

   当公司内还没有临时工的时候,工作太多了,就会将很多工作记录下来。(任务存储在任务队列中) 而当任务多到 本子记不下了的时候,就会执行拒绝策略

3、 ***线程池的工作流程 *** 

(1)最开始的时候,线程池是一个空的。(公司内一个员工也没有)

(2)随着任务的提交,开始创建线程

① if (当前线程数 < corePoolSize ) , 就创建线程 (正式员工)

② if (当前线程数 == corePoolSize ),就把任务添加到工作队列中   (先让正式员工紧着干)

③ 队列满了, if (当前线程数 < maxmumPoolSize ),创建线程(紧着干也干不完了,招临时员工)

④ 队列满了,  if (当前线程数 == maxmumPoolSize ),执行拒绝策略(招临时员工后,也干不完,就拒绝,不接任务了)

(3)随着任务的执行,剩余的任务逐渐减少,逐渐有了空闲的线程:

if (空闲时间 > keepAliveTime , && 当前线程数 > corePoolSize) ,销毁线程

直到 当前线程数 == corePoolSize 。(公司任务少了,不需要临时员工了)

4、 ***如何设置参数 *** 

 *** 如何设置参数?

如果使用线程池的话,多少线程数合适?

   要经过性能测试的方式找到合适的值。

   通过不同的线程池的线程数,来观察 程序处理任务时的速度,以及 程序持有的CPU 的占用率。

  • 当线程数多了,整体的速度是会变快,但是 CPU 的占用率也会变高;
  • 当线程数少了,整体的速度是会变慢,但是 CPU 的占用率也会下降。

   CPU 的占用率不能太高,因为我们需要留有一定的冗余,来处理突发情况。以服务器为例,如果本身就已经比 CPU 快占完了,这是突然来了一大波请求,可能服务器处理不过来就崩溃了。

   因此需要找到一个让 程序速度(接口响应时间尽可能的快)能接受,并且 CPU 占用也合理的 平衡点    

5、 使用标准库中的线程池—— Executors 类

   标准库中还提供了一个 简化版 的线程池 —— Executors 。它是针对 ThreadPoolExecuter 进行了封装,并且提供了一些默认参数。

   其构造函数有返回值,返回值是 ExecutorService

   然后通过 ExecutorService 中的 submit 方法,就可以注册一个任务到线程池中。

(1)创建一个 固定线程数 的线程池,参数指定了线程的个数

        ExecutorService pool = Executors.newFixedThreadPool(10);

(2)创建一个 自动扩容 的线程池,会根据任务量来 自动扩容

        ExecutorService pool2 = Executors.newCachedThreadPool();

(3)创建一个 只有1个线程 的线程池

        ExecutorService pool3 = Executors.newSingleThreadExecutor();

(4)创建一个 给带有定时器功能 的 线程数(类似 Timer,Timer中只有一个线程在执行,当任务过多时,可能就需要多个线程了)

        ExecutorService pool4 = Executors.newScheduledThreadPool();

使用:

public class Demo1 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

6、 自己实现一个 线程池

  大致思路:

  • 1. 描述任务(使用Runnable类)
  • 2. 组织任务(使用 BlockingQueue)
  • 3. 描述工作线程
  • 4. 组织线程
  • 5. 实现往线程中添加任务(实现 submit 方法)

    我们要实现的线程池:一创建一个线程池,就有线程等待任务注册了。这里我们实现传入指定线程数的构造方法来创建线程池。

   因此,自己实现的线程池类中,其构造方法要进行线程的创建,以及有一个方法能将任务注册到 任务队列 中,以便让线程来执行。

   而具体的线程类,可以通过内部类来实现,要求这个线程中的 run 方法可以获取到 任务队列 中的任务,并执行。 

具体代码:

class MyThreadPool {
    //1. 描述一个任务 —— Runnable,不需要额外创建类了
    //2. 组织任务 —— 阻塞队列
    private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
    //3. 描述线程 —— 线程的任务是到 任务队列 中 取出任务,并执行
    //使用内部类来进行描述,继承自 Thread 即可
    private class Worker extends Thread {

        private BlockingQueue<Runnable> queue = null;
        //创建线程对象时就开执行 run 方法
        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        //run 方法
        public void run() {
            //这里需要能取到上面的 任务队列
            //为了能取到外面的 任务队列,在这个类中定义一个队列的属性,并且在构造方法中传入外面任务队列,再赋给内部的队列即可
            while(true) {
                //循环去获取任务队列中的任务
                //如果任务队列为空,就阻塞;如果不为空,就执行
                try {
                    Runnable runnable = queue.take();
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //4. 组织线程 —— List
    private List<Thread> workers = new ArrayList<>();
    //5.构造方法,不断创建线程
    public MyThreadPool(int n) {
        //创建若干个线程,放入上面的数组中
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            //因为继承了 Thread 方法,所以可以直接使用 start 方法创建启动线程
            worker.start();
            workers.add(worker);
        }
    }

    //6. submit 方法,让程序员能够将任务放入线程池(任务队列)
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);
        //for 循环 ,注册 100 个任务,10 个线程执行这 100 个任务。
        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}