1 为什么要使用线程池?
a.假如线程创建的时间是time1,线程执行的时间是time2,线程销毁的时间呢是time3,往往time1+time3>time2,所以频繁的创建线程,会消耗额外的时间
b.如果等到有任务来了,在去创建线程的话效率就会比较低,如不把线程放在某个地方,任务来了,直接把线程拿过来用比较好
c.线程池可以管理控制线程,线程是稀缺资源,如果不停地创建线程会消耗大量的系统资源,还会降低系统的稳定性。使用线程池,可以进行统一分配,方便调优和监控
d.线程可以提供队列,存放缓冲等待执行的任务
2 效果对比
  • 下面我们来模拟一下 100个用户同时访问web端 执行一个操作

普通线程启动方式

 /**
     * 普通处理线程的方法
     */
    @Test
    public void oldMethod() throws InterruptedException {
        //书写一个循环  模拟100个用户同时访问
        for (int request = 0; request< 100;request ++){
            //开启一个线程
            new Thread(()->{
                System.out.println("开始执行...");
                /**
                 * 睡10秒
                 */
                try {
                    Thread.sleep(1000L * 30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("结束执行...");
            }).start();
        }
        //为了方便测试  我们让主线程睡一会
        Thread.sleep(1000 * 1000);
    }

这时我们看下下面的图片的,一共有100个线程在执行,如果要是10000个用户来访问该程序,系统不得直接给干崩溃了!所以我们就得使用下面的线程池
2020最新Java线程池入门(超详细)_创建线程
利用线程池启动线程方式

 @Test
    public void newMethod() throws InterruptedException {
        /**
         * 开启一个线程池  默认线程是10个
         * 使用默认线程工厂
         * 拒绝策略为CallerRunsPolicy策略,让后面的线程先等待 
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 1000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());

            //书写一个循环  模拟100个用户同时访问
            for (int request = 0; request< 100;request ++){
                //开启一个线程
                threadPoolExecutor.execute(() ->{
                    System.out.println("开始执行...");
                    /**
                     * 睡10秒
                     */
                    try {
                        Thread.sleep(1000L * 30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("结束执行...");
                });
            }

        //为了方便测试  我们让主线程睡一会
        Thread.sleep(1000 * 1000);
    }

我们可以看到这时候只有10个线程在跑,剩下的也都在等待该这是个线程跑完,才会有新的线程执行
2020最新Java线程池入门(超详细)_入门_02

3 线程池简介
  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性
4 简单线程池的设计

简单线程池设计思路
要有阻塞队列丶执行器丶线程池子(存放线程)
2020最新Java线程池入门(超详细)_线程池_03

  • 设计过程中要思考的问题
    • 初始创建多少线程?
    • 没有可用线程了怎么办?
    • 缓冲数组要多长?
    • 缓冲数组满了怎么办?
5 线程池的核心参数
  • corePoolSize 核心池大小
  • maximumPoolSize 池中允许的最大线程,这个参数表示了线程池中最多的线程数量
  • keepAliveTime 当线程大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间
  • unit 上一个参数的时间单位
  • workQueue 存储还没来的及执行的任务
  • threadFactory 执行程序创建新线程时使用的工厂
  • handler 由于超出线程范围和队列容量而使执行被阻塞时使用的处理程序
6 线程池的处理流程

从下面这张图我们可以看出:
1.提交任务
2.判断线程数是否达到最大(是则进入判断任务队列)否就提交任务
3.判断任务队列是否已满(是则进入判断最大线程数)否就将任务加在任务队列
4.判断线程达到最大线程数(是则进入饱和策略)否则创建非核心线程执行任务
5.执行饱和策略,根据饱和策略的不同,程序作出不同的响应
2020最新Java线程池入门(超详细)_线程池_04

7 线程池可选择的阻塞队列
  • 无界队列(常用的为无界的LinkedBlockingQueue)
    可以无限创建线程(有风险)
  • 有界队列
    常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
    使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

在我们的修复方案中,选择的就是这个类型的队列,虽然会有部分任务被丢失,但是我们线上是排序日志搜集任务,所以对部分对丢失是可以容忍的。

  • 同步移交队列
    如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
8 线程池可选择的饱和策略
	AbortPolicy终止策略(默认)
	DiscardPolicy抛弃策略
	DiscardOldestPolicy抛弃旧任务策略
	CallerRunsPolicy调用者运行策略
9 线程池的执行示意图

2020最新Java线程池入门(超详细)_java_05

10 常用线程池
	newCachedThreadPool线程数量无限线程池
	newFixedThreadPool线程数量固定线程池
	newSingleThreadExecutor单一线程线程池
11 线程池的状态

五种状态

  • RUNNING
  • SHUTDOWN
  • STOP
  • TIDYING
  • TERMINATED