线程池学习(通俗易懂)

时间:2024-04-19 07:07:17

线程池

  • 线程池是什么
  • ThreadPoolExecutor
  • 模拟实现线程池
  • 结语

线程池是什么

假设我们要频繁的创建线程和销毁线程,但是创建线程和销毁线程是有成本的.
所以我们可以提前创建一批线程,后面需要使用的时候,直接拿就可以了,这就是线程池.
当线程不再使用的时候,就归还到池子里.

为什么从线程池里取比在系统里创建线程更加高效呢?

用系统去创建线程,需要调用系统api,进一步有系统内核完成线程的创建.
(内核是给所有线程提供服务的,这是不可控的)
如果是从线程池里取,上述在内核里的操作都已经提前做好了,取线程的过程,就变为了纯用户态(可控).

在java标准库中,也提供现成的线程池供我们使用.

public static void main(String[] args) {
        // Executors: 工厂类   newFixedThreadPool(int): 工厂方法
        // 工厂模式: 一般创建对象都是通过new来调用构造方法
        // 创建了一个固定数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);
        // 创建一个线程树木动态变化的线程池
        //Executors.newCachedThreadPool();
        // 创建单个线程(比原本系统内核创建线程更简单)
        //Executors.newSingleThreadExecutor();
        // 创建计时器线程.可能是由多个线程共同执行所有的任务
        //Executors.newScheduledThreadPool(2);

        for (int i = 0; i < 20; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello Executors");
                }
            });
        }
    }

// 这里可能会用创建的全部线程去执行,打印20个hello Executors.

什么是工厂模式呢?

一般创建对象都是通过new来调用构造方法,但是构造方法的名字固定就是类名,
有的类就需要多种不同的构造方法,因为构造方法的名字固定,就只能使用方法重载来实现了.
可是还有不能使用方法重载的场景,比如数学中的一个坐标点,
可以使用笛卡尔坐标系的方式,也可以使用极坐标的方式,它们参数的个数和类型相同,无法
构成重载.
当我们使用工厂模式时,不用使用构造方法了,使用普通的方法来构造对象,这样方法的名字就可以是任意的了.由于普通方法的目的是创建对象,这样的方法一般是静态的.

ThreadPoolExecutor

除了上述的线程池之外,标准库还提供了接口更丰富的线程池类: ThreadPoolExecutor.
我么来看看java文档中ThreadPoolExecutor的构造方法,并来学习线程池构造方法的参数和含义.
在这里插入图片描述

  • int corePoolSize : 核心线程数, 在ThreadPoolExecutor里面的线程个数,并非是固定不变的,会根据当前任务的情况动态发生变化,至少得有corePoolSize 线程,哪怕线程池中一点任务也没有.
  • int maximumPoolSize: 最大线程数: maximumPoolSize表示最多的线程数,不能比这个数目更多了.
  • long keepAliveTime, TimeUnit unit : 分别表示时间和单位, 比如3000, ms, 这时就是3s.当线程超过制定时间阈值后就可以销毁了.
  • BlockingQueue workQueue: 线程中有很多任务,这些任务可以用阻塞队列来管理.
  • ThreadFactory threadFactory: 工厂模式,通过这个工厂类来创建线程.
  • RejectedExecutionHandler handler(非常重要,重点掌握): 拒绝方式/拒绝策略.我们知道,线程池中有一个阻塞队列,当阻塞队列满的时候,继续添加任务,该如何应对???
    (1) ThreadPoolExecutor.AbortPolity: 直接抛出异常,线程池就不干活了.
    (2) ThreadPoolExecutor.callerRunsPolity : 谁是添加这个新任务的线程,谁就去执行这个任务.
    (3) ThreadPoolExecutor.DiscardOldestPolity: 丢弃最早的任务,执行新的任务.
    (4) ThreadPoolExecutor.DiscardPolity: 直接把新的任务丢弃掉.

模拟实现线程池

这里我们实现一个固定数量的线程池:

class MyThreadPool {
    private final BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    // 添加任务
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    // 创建一个固定数量的线程池
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            thread.start();
        }
    }
}
public class Demo26 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(4);
        for(int i = 0; i < 1000; i++)  {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello pool");
                }
            });
        }
    }
}

在我们创建线程池的时候,线程个数是哪来的?

  1. 有的线程的工作是"CPU密集型", 线程的工作全是运算.大部分的工作是在CPU上完成的,CPU得给它安排核心去完成工作才可以有进展.如果CPU是N个核心,当线程数量也是N的时候.这是理想情况,每个核心上一个线程.如果有很多的线程,就会阻塞等待.
  2. 有的线程是的工作,是"IO密集型", 会涉及大量的等待时间,就算线程数量多一点,也不会给CPU造成太大的负担.

在实际开发中,往往通过尝试不同的线程数,来找到合适的线程数,找到性能和系统资源开销比较均衡的数值.

结语

本篇博客总结了线程池相关的知识,满满的干货,希望有收获的小伙伴多多支持!