详解Java线程池

工作原理:
Java线程池主要用于管理线程组及其运行状态,以便Java虚拟机更好的利用CPU资源。Java线程池的工作原理为:JVM先根据用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量(用户设置的线程池大小),则超出数量的线程排队等候,在有任务执行后,线程池调度器会发现可用的线程,进而再次从队列中取出任务并执行。

线程池的主要作用是线程复用、线程资源管理、控制操作系统的最大并发数,以保证系统高效(通过线程资源复用实现)且安全(通过控制最大线程并发数实现)地运行。

线程复用:
在Java中,每个Thread类都有一个start方法。在程序调用start方法启动线程时,Java虚拟机会调用该类的run方法。Java中的线程Thread类的run方法中其实调用了Runnable对象的run方法,因此可以继承Thread 类 , 在start方法中不断循环调用传递进的Runnable对象,程序就会不断执行run方法中的代码。可以在循环方法中不断获取放在Queue中的Runnable对象,当前线程在获取下一个Runnable对象之前可以是阻塞的,这样既能有效控制正在执行的线程个数,也能保证系统中正在等待执行的其他线程有序执行。这样就简单实现了一个线程池,达到了线程复用的效果。

核心组件和核心类:
Java线程池主要由以下4个核心组件组成:

  1. 线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中执行具体任务的线程
  3. 任务接口:用于定义工作线程的调度和执行策略,只有线程实现了该接口,线程中的任务才能够被线程池调度
  4. 任务队列:存放待处理的任务,新的任务将会不断被加入队列中,执行完成的任务将被从队列中移除

Java中的线程池是通过Executor框架实现的,在该框架中用到了Executor 、 Executors 、 ExecutorService 、ThreadPoolExecutor、Callable、Future、FutureTask这几个核心类,具体的继承关系下图所示:
在这里插入图片描述

在这里插入图片描述
其中,ThreadPoolExecutor是构建线程的核心方法,该方法的
定义如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
            // ......
            // 省略了赋值操作 
            // ......            
}

ThreadPoolExecutor构造函数的具体参数如下表所示:

序号参数说明
1corePoolSize线程池中核心线程的数量
2maximumPoolSize线程池中最大线程的数量
3keepAliveTime当前线程数量超过corePoolSize 时,空闲线程的存活时间
4unitkeepAliveTime 的时间单位
5workQueue任务队列,被提交但尚未被执行的任务存放的地方
6threadFactory线程工厂,用于创建线程,可使用默认的线程工厂或自定义线程工厂
7handler由于任务过多或其他原因导致线程池无法处理时的任务拒绝策略

工作流程:
Java线程池的工作流程为:线程池刚被创建时,只是向系统申请一个用于执行线程队列和管理线程池的线程资源。在调用execute()添加一个任务时,线程池会按照以下流程执行任务:

  • 如果正在运行的线程数量少于corePoolSize(用户定义的核心线程数),线程池就会立刻创建线程并执行该线程任务
  • 如果正在运行的线程数量大于等于corePoolSize,该任务就将被放入阻塞队列中
  • 在阻塞队列已满且正在运行的线程数量少于maximumPoolSize时,线程池会创建非核心线程立刻执行该线程任务
  • 在阻塞队列已满且正在运行的线程数量大于等于maximumPoolSize时,线 程池将拒绝执行该线程任务并抛出RejectExecutionException异常
  • 在线程任务执行完毕后,该任务将被从任务队列中移除,线程池将从队列中取下一个线程任务继续执行
  • 在线程处于空闲状态的时间超过keepAliveTime时间时,正在运行的线程数量超过corePoolSize,该线程将会被认定为空闲线程并停止。因此在线程池中所有线程任务都执行完毕后,线程池会收缩到corePoolSize大小

具体的流程如下图所示:
在这里插入图片描述
拒绝策略:
若线程池中的线程数被用完且阻塞队列已排满,则此时线程池的线程资源已耗尽,线程池将没有足够的线程资源执行新的任务。为了保证操作系统的安全,线程池将通过拒绝策略处理新添加的线程任务。JDK内置的拒绝策略有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy这 4种,这些拒绝策略在ThreadPoolExecutor中作为内部类提供,默认的拒绝策略是AbortPolicy。这些拒绝策略不能满足应用的需求时,可以自定义拒绝策略。

  1. AbortPolicy
    AbortPolicy直接抛出异常,阻止线程正常运行,具体的JDK源码如下:
public static class AbortPolicy implements RejectedExecutionHandler {
        public AbortPolicy() {}
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  1. CallerRunsPolicy
    直接在方法的调用线程中执行,除非线程池已关闭,具体的JDK实现源码如下:
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() {}
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
  1. DiscardPolicy
    丢弃当前的线程任务而不做任何处理。如果系统允许在资源不足的情况下弃部分任务,则这将是保障系统安全、稳定的一种很好的方案。具体的JDK实现源码如下:
public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() {}
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
  1. DiscardOldestPolicy
    移除线程队列中最早(老)的一个线程任务,并尝试提交当前任务。具体的JDK实现源码如下:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();// 最早(老)的任务出队列
                e.execute(r);
            }
        }
    }
  1. 自定义拒绝策略(后文手写线程池时再实现
    以上4种拒绝策略均实现了RejectedExecutionHandler接口,若无法满足实际需要,则用户可以自己扩展RejectedExecutionHandler接口来实现拒绝策略,并捕获异常来实现自定义拒绝策略。

5种常用的线程池:
1、newCachedThreadPool(可缓存的线程池)
创建方式:

ExecutorService threadPool = Executors.newCachedThreadPool();

具体源码:
在这里插入图片描述
看源码可知,newCachedThreadPool的核心线程数量为0,也就是说创建的都是非核心线程,并且在空闲时间超过60秒后会自动销毁,因此在没有线程任务运行时,newCachedThreadPool将不会占用系统的线程资源。

使用场景:
任务数量密集而每个任务的执行时间短的情况,newCachedThreadPool能够很好地复用运行中的线程(任务已经完成但未关闭的线程)资源来提高系统的运行效率。

2、newFixedThreadPool(固定大小的线程池)
创建方式:

ExecutorService threadPool = Executors.newFixedThreadPool(2);

具体源码:
在这里插入图片描述
可以看出,newFixedThreadPool的核心线程数量与最大线程数量大小是一样的,说明创建的都是核心线程,没有非核心线程。

使用场景:
适用于任务数量已知,相对耗时的任务。

3、newSingleThreadExecutor(单个线程的线程池)
创建方式:

ExecutorService threadPool = Executors.newSingleThreadExecutor();

具体源码:
在这里插入图片描述
可以看出,newSingleThreadExecutor的核心线程数量和最大线程数量都为1,该线程池会保证永远有且只有一个可用的线 程,在该线程停止或发生异常时,newSingleThreadExecutor线程池会启动一个新的线程来代替该线程继续执行任务。

使用场景:
适用于希望任务按顺序执行的场景。

4、newScheduledThreadPool(可做任务调度的线程池)
创建方式:
在这里插入图片描述
具体源码:
在这里插入图片描述
在这里插入图片描述
newScheduledThreadPool创建了一个可定时调度的线程池,可设置在给定的延迟时间后执行或者定期执行某个线程任务。

使用场景:
适用于需要定时调度定期执行任务的场景。

5、newWorkStealingPool(工作窃取线程池,JDK1.8新增)
创建方式:

public class Main {

    static class Task implements Runnable{
        private int time;
        public Task(int time){this.time = time;}
        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "  " + time);
        }
    }

    public static void main(String[] args) throws IOException {
        // CPU 核数
        int num = Runtime.getRuntime().availableProcessors();
        System.out.println(num);

        // workStealingPool会自动启动cpu核数个线程去执行任务
        ExecutorService service = Executors.newWorkStealingPool();
        service.execute(new Task(1000));
        // 有一个任务会进行等待,当第一个执行完毕后,会再次偷取任务执行
        for (int i = 0; i < num; i++) {
            service.execute(new Task(2000));
        }

        // 因为work stealing 是deamon线程,即后台线程,精灵线程,守护线程
        // 所以当main方法结束时, 此方法虽然还在后台运行,但是无输出
        // 可以通过对主线程阻塞解决
        System.in.read();
    }
}

具体源码:
在这里插入图片描述
可以看出,WorkStealingPool背后是使用ForkJoinPool实现的,构造函数的第一个参数就是系统CPU核数,这样很大程度地使用系统资源,提高并发计算的效率,省去用户根据CPU资源估算并行度的过程。在内部通过使用多个队列来减少各个线程调度产生的竞争。当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。

好了,5种线程池就介绍完了,下面随我一步步实现一个自定义的线程池:

1、定义一个阻塞队列用于存放和获取任务

class BlockingQueue<T> {
    //任务队列
    private Deque<T> queue = new ArrayDeque<>();

    //锁
    private ReentrantLock lock = new ReentrantLock();

    //生产者条件变量
    private Condition fullWaitSet = lock.newCondition();

    //消费者条件变量
    private Condition emptyWaitSet = lock.newCondition();

    //容量
    private int capcity;

    public BlockingQueue(int capcity) {
        this.capcity = capcity;
    }

    //阻塞获取任务
    public T take(){
        lock.lock();
        try{
            while (queue.isEmpty()){
                try {
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T t = queue.removeFirst();
            fullWaitSet.signal();
            return t;
        }finally {
            lock.unlock();
        }
    }

    //阻塞添加任务
    public void put(T task){
        lock.lock();
        try{
            while (queue.size() == capcity){
                try {
                    fullWaitSet.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            queue.addLast(task);
            emptyWaitSet.signal();
        }finally {
            lock.unlock();
        }
    }

public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
        lock.lock();
        try {
            //判断队列是否满
            if (queue.size() == capcity) {
                rejectPolicy.reject(this,task);
            }else {  //有空闲
                queue.addLast(task);
                emptyWaitSet.signal();
            }
        }finally {
            lock.unlock();
        }
    }

    //获取大小
    public int size(){
        lock.lock();
        try{
            return queue.size();
        }finally {
            lock.unlock();
        }
    }
}

由于篇幅原因,该阻塞队列仅提供了阻塞的添加获取任务的基本方法,后续可完善。

2、定义工作线程

class Worker extends Thread{
        private Runnable task;

        public Worker(Runnable task){
            this.task = task;
        }
		@Override
        public void run(){
            //当task不为空,执行任务
            //当task执行完毕,再接着从任务队列中获取任务并执行
            while (task != null || (task = taskQueue.take()) != null){
                try{
                    task.run();
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    task = null;
                }
            }
        }
    }

该类是线程池类的内部类,这里先定义好。

3、定义拒绝策略接口

interface RejectPolicy<T> {
    void reject(BlockingQueue<T> queue,T task);
}

4、定义线程池类

class ThreadPool{
    //任务队列
    private BlockingQueue<Runnable> taskQueue;

    //线程集合
    private HashSet<Worker> workers = new HashSet<>();

    //核心线程数
    private int coreSize;
    
	// 拒绝策略
    private RejectPolicy<Runnable> rejectPolicy;

    //执行任务
    public void execute(Runnable task){
        //当线程数没有超过 coreSize时,新建worker 对象执行
        //如果任务数超过 coreSize时,加入任务队列暂存
        synchronized(workers){
            if (workers.size() < coreSize) {
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start();
            }else {
                taskQueue.tryPut(rejectPolicy,task);
            }
        }
    }

    public ThreadPool(int coreSize, int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
        this.coreSize = coreSize;
        this.taskQueue = new BlockingQueue<>(queueCapcity);
        this.rejectPolicy = rejectPolicy;
    }

    class Worker extends Thread{
        // 前面已介绍,这里省略了相关属性和方法
	}
}

5、测试类

public class TestThreadPool {
    public static void main(String[] args) {
        ThreadPool threadPool = new ThreadPool(2, 10
        ,(queue,task) -> {
            //死等
            //queue.put(task);
            //让调用者抛出异常
            //throw new RuntimeException("任务执行失败 "+task);
            //让调用者自己执行任务
            task.run();
        });
        for (int i = 0;i<5;i++){
            threadPool.execute(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

好了,自定义的线程池实现完毕,虽然写的比较简陋,但线程池的大致运行流程如此。

至此,Java中的线程池详解完毕,最后介绍一下常用的7种阻塞队列并附上一张类图

名称说明
ArrayBlockingQueue一个由数组结构组成的有界阻塞队列
LinkedBlockingQueue一个由链表结构组成的有界阻塞队列
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列
DelayQueue一个使用优先级队列实现的无界阻塞队列
SynchronousQueue一个不存储元素的阻塞队列
LinkedTransferQueue一个由链表结构组成的无界阻塞队列
LinkedBlockingDeque一个由链表结构组成的双向阻塞队列

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

b17a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值