java中线程池的实现原理:七参、四策

七参-前言

回归最原本的问题,从一个最简单的情况开始,假设有一段代码run() Runnable Thread,你希望异步执行它,怎么写?

new Thread(r).start();//线程创建    销毁--等待GC

这是最简单最直接的写法,我们必须肯定的是:这种写法当然是可以完成功能的。

可是大家都这样写,到处都是这样创建线程的方法,能不能写一个统一的工具类让大家调用呢?发布任务者不用操心谁来做,只需要知道任务结果即可。显得更加优雅一些。

定义一个接口,让各个调用者进行实现

public interface Executor{
	public void execute(Runnable r);
}

第一版 实现类

// 生产者:用户-->Runnable  线程池:线程-->消费tasks
public class ExecutorV1 implements Executor {

    @Override
    public void execute(Runnable command) {
        new Thread(command).start();
    }
}

Doug Lea 在JDK源码注释中给出的就是这样的例子,这是最根本的功能。

虽然此工具类实现了异步执行任务,但是:假设由10000个用户都在调用这个工具类提交任务,那就会创建10000个线程来执行!!

不合适:系统资源开销 对象管理

能不能控制线程的数量?

Executors.newFixedThreadPool(int x)

第二版 实现类

设计一个tasks队列,把所有用户提交的不同需求都给放在这个队列里,然后只启动一个线程,就叫Worker线程,不断从tasks队列中取任务,执行任务。这样无论调用者调用多少次,永远就只有一个Worker线程在运行,如图:

threadpool1

public class ExecutorV2 implements Executor {

    //由调用者提供的阻塞队列
    private final BlockingQueue<Runnable> workQueue;

    public ExecutorV2(BlockingQueue<Runnable> workQueue){
        this.workQueue = workQueue;
        new Thread(new Worker()).start();
    }

    @Override
    public void execute(Runnable command) {
        //直接往队列里放,等着被工作线程们抢
        // Thread1--queue.offer()-->BlockingQueue---queue.take()-->Thread2
        if(!workQueue.offer(command)){ //true   false
            //如果队列满了,直接抛弃
            System.out.println("队列满了,直接抛弃");
        }
    }

    private final class Worker implements Runnable{

        //死循环从队列里读任务,然后运行任务
        @Override
        public void run() {
            Runnable task;
            while(true){
                if((task=getTask())!=null){
                    task.run();
                }
            }
        }

        //堵塞地从队列里获取一个任务
        private Runnable getTask(){
            try {
                return workQueue.take();
            } catch (InterruptedException e) {
                return null;
            }
        }
    }
}

意义有三个:

  1. 控制了线程数量
  2. 队列不但起到了缓冲的作用,还将任务的提交与执行解耦了
  3. 最重要的一点是,解决了每次重复创建和销毁线程带来的系统开销

只有一个后台的工作线程Worker会不会少了点?tasks队列满了怎么办?

第三版 实现类

Worker线程数量增加,但具体数量要让使用者决定,调用时传入,就叫核心线程数corePoolSize

设计思路:

  1. 初始化线程池时,直接启动corePoolSize个工作线程Worker跑着
  2. Worker的任务就是死循环从队列里取任务然后执行
  3. execute方法直接获取任务,把任务放进队列,但队列满了之后直接抛弃

threadpool2

public class ExecutorV3 implements Executor {

    //由调用者提供的阻塞队列
    private final BlockingQueue<Runnable> workQueue;

    public ExecutorV3(int corePoolSize,BlockingQueue<Runnable> workQueue){
        this.workQueue = workQueue;
        //直接创建corePoolSize个线程并启动
        for (int i = 0; i < corePoolSize; i++) {
            new Thread(new Worker()).start();
        }
    }

    @Override
    public void execute(Runnable command) {
        //直接往队列里放,等着被工作线程们抢
        if(!workQueue.offer(command)){
            //如果队列满了,直接抛弃
            System.out.println("队列满了,直接抛弃");
        }
    }

    private final class Worker implements Runnable{

        //死循环从队列里读任务,然后运行任务
        @Override
        public void run() {
           Runnable task;
           while(true){
               if((task=getTask())!=null){
                   task.run();
               }
           }
        }

        //阻塞地从队列里获取一个任务
        private Runnable getTask(){
            try {
                return workQueue.take();
            } catch (InterruptedException e) {
                return null;
            }
        }
    }
}

问题

  1. 初始化时,创建corePoolSize个线程在那空跑,但没有异步任务提交过来,就有点浪费资源了
  2. 队列一满,直接丢弃任务,有些粗暴,是否让调用者自己决定如何处理

第四版 实现类

  1. 按需创建Worker:刚初始化线程池时,不再立刻创建 corePoolSize 个工作线程,而是等待调用者不断提交任务的过程中,逐渐把工作线程 Worker 创建出来,等数量达到 corePoolSize 时就停止,把任务直接丢到队列里。那就必然要用一个属性记录已经创建出来的工作线程数量,就叫 workCount
  2. 加拒绝策略:实现上就是增加一个入参,类型是一个接口 RejectedExecutionHandler,由调用者决定实现类,以便在任务提交失败后执行 rejectedExecution 方法。
  3. 增加线程工厂:实现上就是增加一个入参,类型是一个接口 ThreadFactory,增加工作线程时不再直接 new 线程,而是调用这个由调用者传入的 ThreadFactory 实现类的 newThread 方法。

threadpool3x

public class ExecutorV4 implements Executor {

    // 工作线程的数量
    private AtomicInteger workCount = new AtomicInteger(0);
    // 存放工作线程 Worker 的引用
    private final HashSet<Worker> workers = new HashSet<>();

    // 核心线程数量(超过了就放队列里)
    private volatile int corePoolSize;
    // 由调用者提供的阻塞队列,核心线程数满了之后往这里放
    private final BlockingQueue<Runnable> workQueue;
    // 拒绝策略
    private volatile RejectedExecutionHandler handler;
    // 线程工厂
    private volatile ThreadFactory threadFactory;

    public ExecutorV4(
            int corePoolSize,
            BlockingQueue<Runnable> workQueue,
            RejectedExecutionHandler handler,
            ThreadFactory threadFactory) {
        this.corePoolSize = corePoolSize;
        this.workQueue = workQueue;
        this.handler = handler;
        this.threadFactory = threadFactory;
    }

    @Override
    public void execute(Runnable command) {
        if (workCount.get() < corePoolSize) {
            // 工作线程数 <= 核心线程时,新建工作线程
            Worker w = new Worker(command);
            // 增加工作线程数
            workCount.getAndIncrement();
            workers.add(w);
            // 并且把它启动
            w.thread.start();
        } else {
            // 工作线程数 > 核心线程时,放入队列
            if (!workQueue.offer(command)) {
                // 放入队列失败,走拒绝策略
                handler.rejectedExecution(command, this);
            }
        }
    }

    private final class Worker implements Runnable {

        final Thread thread;
        private Runnable task;

        Worker(Runnable firstTask) {
            this.task = firstTask;
            this.thread = threadFactory.newThread(this);
        }

        // 死循环从队列里读任务,然后运行任务
        @Override
        public void run() {
            while (task != null || (task = getTask()) != null) {
                task.run();
                task = null;
            }
        }

        // 阻塞地从队列里获取一个任务
        private Runnable getTask() {
            try {
                return workQueue.take();
            } catch (InterruptedException e) {
                return null;
            }
        }

    }
}

第五版 实现类

问题:

  1. 任务提交频繁
  2. 任务提交不频繁

上述代码是否有问题?

当任务提交量集中爆发,工作线程和队列都被占满了,就只能走拒绝策略,被丢弃掉

threadpool4

解决办法

设置很大的核心线程数corePoolSize来解决这个问题,是可以的,但是高峰期一般很短暂,为了短暂的高峰设置很大的核心线程数,太浪费资源了。

threadpool5

不够有弹性

弹性思维

设置一个新的属性,最大线程数**maximumPoolSize。当核心线程数和队列都满了时,新提交的任务仍然可以通过创建新的工作线程(叫它非核心线程**),直到工作线程数达到 maximumPoolSize 为止,这样就可以缓解一时的高峰期了,而用户也不用设置过大的核心线程数。

threadpool6

threadpool7

  1. 开始的时候和上一版一样,当 workCount < corePoolSize 时,通过创建新的 Worker 来执行任务。
  2. 当 workCount >= corePoolSize 就停止创建新线程,把任务直接丢到队列里。
  3. 但当队列已满且仍然 workCount < maximumPoolSize 时,不再直接走拒绝策略,而是创建非核心线程,直到 workCount = maximumPoolSize,再走拒绝策略。

corePoolSize 就负责平时大多数情况所需要的工作线程数,而 maximumPoolSize 就负责在高峰期临时扩充工作线程数。

高峰时期的弹性搞定了,那自然就还要考虑低谷时期。当长时间没有任务提交时,核心线程与非核心线程都一直空跑着,浪费资源。我们可以给非核心线程设定一个超时时间 keepAliveTime,当这么长时间没能从队列里获取任务时,就不再等了,销毁线程。

threadpool8

线程池在 QPS 高峰时可以临时扩容,QPS 低谷时又可以及时回收线程(非核心线程)而不至于浪费资源

public class ExecutorV5 implements Executor {
    // 工作线程的数量
    private AtomicInteger workCount = new AtomicInteger(0);
    // 存放工作线程 Worker 的引用
    private final HashSet<Worker> workers = new HashSet<>();

    // 核心线程数量
    private volatile int corePoolSize;
    // 最大线程数
    private volatile int maximumPoolSize;
    // 空闲时间
    private volatile long keepAliveTime;
    // 空闲时间
    private TimeUnit timeUnit;
    // 由调用者提供的阻塞队列,核心线程数满了之后往这里放
    private final BlockingQueue<Runnable> workQueue;
    // 拒绝策略
    private volatile RejectedExecutionHandler handler;
    // 线程工厂
    private volatile ThreadFactory threadFactory;

    public ExecutorV5(
            int corePoolSize,
            int maximumPoolSize,
            long keepAliveTime,
            TimeUnit timeUnit,
            BlockingQueue<Runnable> workQueue,
            RejectedExecutionHandler handler,
            ThreadFactory threadFactory) {
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.timeUnit = timeUnit;
        this.workQueue = workQueue;
        this.handler = handler;
        this.threadFactory = threadFactory;
    }

    @Override
    public void execute(Runnable command) {
        if (workCount.get() < corePoolSize) {
            addWorker(command);
            return;
        }
        if (!workQueue.offer(command)) {
            if (workCount.get() < maximumPoolSize) {
                addWorker(command);
                return;
            }
            handler.rejectedExecution(command, this);
        }
    }

    private void addWorker(Runnable command) {
        // 工作线程数 <= 核心线程时,新建工作线程
        Worker w = new Worker(command);
        // 增加工作线程数
        workCount.getAndIncrement();
        workers.add(w);
        // 并且把它启动
        w.thread.start();
    }


    private final class Worker implements Runnable {

        final Thread thread;
        private Runnable task;

        Worker(Runnable firstTask) {
            this.task = firstTask;
            this.thread = threadFactory.newThread(this);
        }

        // 死循环从队列里读任务,然后运行任务
        @Override
        public void run() {
            while (task != null || (task = getTask()) != null) {
                task.run();
                task = null;
            }
            workCount.getAndDecrement();
        }

        // 阻塞地从队列里获取一个任务
        private Runnable getTask() {
            boolean timed = workCount.get() > corePoolSize;
            try {
                return timed ? workQueue.poll(keepAliveTime, timeUnit) : workQueue.take();
            } catch (InterruptedException e) {
                return null;
            }
        }

    }
}

总结

七大核心参数:

  • int corePoolSize:核心线程数
  • int maximumPoolSize:最大线程数
  • long keepAliveTime:非核心线程的空闲时间
  • TimeUnit unit:空闲时间的单位
  • BlockingQueue workQueue:任务队列(线程安全的阻塞队列)
  • ThreadFactory threadFactory:线程工厂
  • RejectedExecutionHandler handler:拒绝策略

整个任务的提交流程:

threadpool9

拒绝策略

当线程池的任务缓存阻塞队列已满,并且线程池中的线程数**workCount== maximunPoolSize**时,如果还有任务提交就会采取拒绝策略。

四种拒绝策略:

都是RejectedExecutionHandler子类,都是静态内部类,都有两个方法:构造方法rejectedExecution方法

  1. ThreadPoolExecutor.AbortPolicy【默认的】

    源码

    内部类AbortPolicy注释

    A handler for rejected tasks that throws a RejectedExecutionException.

    一种拒绝任务的处理程序:抛出RejectedExecutionException

    方法rejectedExecution(Runnable r,ThreadPoolExecutor e)注释

    Always throws RejectedExecutionException.

    总是抛出RejectedExecutionException

  2. ThreadPoolExecutor.DiscardPolicy

    源码

    内部类DiscardPolicy注释

    A handler for rejected tasks that silently discards the rejected task.

    一种拒绝任务的处理程序:将自动放弃被拒绝的任务

    方法rejectedExecution(Runnable r,ThreadPoolExecutor e)注释

    Does nothing, which has the effect of discarding task r.

    不执行任何操作,具有丢弃任务r的效果

  3. ThreadPoolExecutor.DiscardOldestPolicy

    源码

    内部类DiscardOldestPolicy注释

    A handler for rejected tasks that discards the oldest unhandled request and then retries execute, unless the executor is shut down, in which case the task is discarded.

    一种拒绝任务的处理程序:丢弃最早的未处理请求,然后重试执行,除非线程池关闭,在这种情况下,任务被丢弃

    方法rejectedExecution(Runnable r,ThreadPoolExecutor e)注释

    Obtains and ignores the next task that the executor would otherwise execute, if one is immediately available, and then retries execution of task r, unless the executor is shut down, in which case task r is instead discarded.

    获取并忽略线程池将执行的下一个任务,如果一个任务立即可用,那么重试执行任务r,除非关闭执行器,在这种情况下,任务r将被丢弃。

  4. ThreadPoolExecutor.CallerRunsPolicy

    源码

    静态内部类CallerRunsPolicy注释

    A handler for rejected tasks that runs the rejected task directly in the calling thread of the execute method, unless the executor has been shut down, in which case the task is discarded.

    一种拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务,除非线程池关闭,在这种情况下,该任务被丢弃

    方法rejectedExecution(Runnable r,ThreadPoolExecutor e)注释

    Executes task r in the caller’s thread, unless the executor has been shut down, in which case the task is discarded.

    在调用者的线程中执行任务r,除非线程池已经关闭,在这种情况下,任务被丢弃。

策略选择

1.默认情况

在线程池的具体实现类java.util.concurrent.ThreadPoolExecutor类中,有如下源码:

/**
 * The default rejected execution handler
 */
private static final RejectedExecutionHandler defaultHandler =
    new AbortPolicy();

线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常。使用代码进行验证:

import java.util.concurrent.*;

public class TestRejectedHandler {
    public static void main(String[] args) {
        //定义一个有界阻塞队列,容量为100
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
        //ThreadFactory是接口,使用Lambda构建对象
        ThreadFactory factory = r -> new Thread(r, "test-thread-pool");
        //创建线程池对象,但唯独不传拒绝策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.SECONDS, queue, factory);
        while (true) {
            //死循环提交任务
            executor.submit(
                    //Lambda构建Runnable实例对象
                    () -> {
                        //run() 核心代码
                        try {
                            System.out.println(queue.size());
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
        }
    }
}

运行后,出现如下效果,是符合预期的:

threadPool-Handler-1

2.常用策略CallerRunsPolicy

如果任务被拒绝,则由调用线程(提交任务的线程)直接执行此任务。代码验证:

import java.util.concurrent.*;

public class TestRejectedHandlerDemo2 {
    public static void main(String[] args) {
        //定义一个有界阻塞队列,容量为10
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
        //ThreadFactory是接口,使用Lambda构建对象
        ThreadFactory factory = r -> new Thread(r, "test-thread-pool");
        //创建线程池对象,传CallerRunsPolicy拒绝策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.SECONDS, queue, factory, new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < 1000; i++) {
            executor.submit(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + ":执行任务");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

运行后,效果如下:

threadPool-Handler-2

可以看到main线程在执行任务,这正说明了此拒绝策略由调用线程(提交任务的线程)直接执行被丢弃的任务

总结:使用场景

  1. AbortPolicy:线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。关键的业务,推荐使用此策略,在系统不能承载更大的并发量的时候,能及时的通过异常发现。
  2. DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
  3. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。是一种喜新厌旧的拒绝策略。是否采用此拒绝策略?根据实际业务是否允许丢弃老任务来抉择
  4. CallerRunsPolicy:由调用线程处理被拒绝的任务

面试题

JDK源码中书写静态内部类的作用

阻塞队列的好处?

现有三个角色:顾客,休息区,银行办理窗口

Thread1为顾客,BlockingQueue为休息区,Thread2为银行办理窗口

  1. 正常情况下,一个银行办理窗口同一时间只能对接一个顾客
  2. 恰巧今天办理的顾客有3个人,另外2个顾客怎么办,你总不至于给人家说不办了,快回家吧
  3. 而正确的做法是你可以让这两个人在休息区等候,等银行窗口空闲了,然后去办理

一个人正在银行办理业务,你后面的人不能打断(保证了原子性),或者争抢(有序性,先进先出),只能在休息区等待,直到上一个人释放资源,才轮到下一个人

问题:

​ 当一个线程占有资源的时候,你后面线程请求不得不阻塞。

但这个也不一定是缺点,反而更像一件好事,因为并不暴力的解决问题。

阻塞定义:

在多线程中,阻塞的意思是,在某些情况下会挂起线程,一旦条件成熟,被阻塞的线程就会被自动唤醒。

阻塞队列的好处:

线程的wait和notify机制不需要自己去手动控制,简化了操作,避免了手动操作容易出现的死锁,逻辑判断等问题。

线程池用了什么设计模式?

线程池模式

策略模式

享元模式

代理模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值