【Java多线程案例】-线程池

Java多线程—线程池

1.1 线程池引入的原因

Java编程世界中是不鼓励多进程编程模式的,因为进程频繁创建与销毁带来了巨大的开销,线程作为轻量级的进程可以有效缓解此问题,但是现在随着线程数目的增多,所带来的的开销也不可忽略!解决的策略有如下两种:

  1. 创建轻量级的线程,又被称为协程/纤程(即Java21引入的虚拟线程)
  2. 使用线程池管理线程,减少启动销毁线程的开销

因此我们此处引入线程池的目的就是有效减少频繁创建线程、销毁线程的带来的开销。

在这里插入图片描述

线程池的实现思路:

  • 提前准备好多个线程存放到线程池中
  • 若添加任务,则直接使用线程池中的线程完成该任务,无需再创建线程
  • 任务执行完毕后,无需将线程销毁,该线程仍交由线程池进行管理

1.2 Java线程池的参数介绍

JVM实现了对线程池的支持,JavaAPI提供了一个线程池实现类ThreadPoolExecutor,其中我们需要了解该类的构造方法中的参数,这对于我们理解线程池该数据结构具有一定意义。

在这里插入图片描述

我们从上述官方文档中可以抽取出核心的参数:

参数名含义
int corePoolSize最大核心线程数
int maximumPoolSize最大线程数
long keepAliveTime线程数存活时间
TimeUnit unitkeepAliveTime的单位
BlockingQueue workQueue阻塞队列(存放缓存任务)
RejectedExecutionHandler拒绝执行处理器

下面我们将对各个参数的含义做出解释:

  • corePoolSize:表示该线程池中的核心线程数目的最大值

  • maximumPoolSize:表示该线程池中线程数目的最大值

  • workQueue:用户缓存任务的阻塞队列

我们通过向线程池中添加任务来说明三者之间的关系

(1) 如果此时没有线程空闲并且线程数小于corePoolSize,那么就添加新的线程并由该线程处理该任务

(2) 如果此时没有线程空闲,且当前线程数等于corePoolSize,但是阻塞队列workQueue此时未满,那么就将该任务添加到阻塞队列中,等到核心线程空闲时进行处理,不添加新的线程。

(3) 如果此时没有线程空闲,并且阻塞队列已经满了,但是线程数目小于maximumPoolSize,此时就添加新的线程执行任务

(4) 如果此时没有线程空闲,且阻塞队列已满,且池中线程数等于maximumPoolSize,此时则根据RejectedExecutionHandler指定的策略拒绝

我们举个生动形象的栗子:

一个公司正常运转,①情景一:如果此时来了一个任务,但是公司员工都处于繁忙状态中,此时老板发现正式工还没有招满,于是老板就招了一个正式工来完成这个任务。此时这个正式工就是核心线程。②情景二:此时又来了一个新的任务,但是此时正式员工已经招满了并且均处于忙碌状态。于是老板决定先将该任务搁置写在备忘录上,这个备忘录就是workQueue,等到正式员工有人忙完手头上的工作时,老板就可以将备忘录上的任务指派给他。③情景三:但是到了年底,正式工满员且都非常忙碌,备忘录上的清单都快列不下了,老板一看不行呀,就招了几个临时工来完成任务,这些临时工就是临时线程。

  • keepAliveTime:表示空闲线程的存活时间
  • unit:表示keepAliveTime的单位

我们继续在上述案例的基础上解释这两个参数的实际含义,接上文老板招了几个临时工完成任务,此后没有新增任务,随着员工各自处理完了手头上的工作。一定有员工闲下来了,但是老板为了节省成本想辞退空闲的员工,但是又担心之后任务是否又会激增,于是老板想了一个策略,若员工空闲时间超过了keepAliveTime就辞退该员工。

  • ThreadFactory:指定创建线程的工厂(工厂模式)

  • RejectedExecutorHandler:指定拒绝策略

为了解释RejectedExecutionHandler的含义,我们在上述案例的基础上扩展新的情景:

此时公司的员工数目已达上限(正式员工+临时工),并且此时备忘录也存放不下了,此时又来了新的任务,老板只能含泪拒绝执行该任务。但是拒绝是一门艺术,如何采用拒绝的策略是有讲究的!

策略含义
ThreadPoolExecutor.AbortPolicy()抛出RejectedExecutionException异常
ThreadPoolExector.CallerRunsPolicy()由提交任务的线程处理该任务
ThreadPoolExecutor.DiscardPolicy()抛弃当前任务
ThreadPoolExecutor.DiscardOldestPolicy()抛弃最先提交但仍未执行的任务

其中AbortPolicy抛出异常,由开发人员针对实际情况进行处理,CallerRunsPolicy表示该任务线程池不进行处理,交由提交线程的任务进行处理,DiscardPolicy表示线程池按照原来的策略进行指定,新任务丢弃不处理,DiscardOldestPolicy表示线程池处理新任务,但是从原来任务中取出最先提交但是并未执行的任务选择抛弃它

1.3 Java线程池的使用

在1.2中我们提到Java提供了线程池的实现类ThreadPoolExecutor,但是我们还是习惯上使用Executors该类作为线程池,该类实际上是对ThreadPoolExecutor的再次封装

public class ThreadDemo01 {
    public static void main(String[] args) {
        // 1. 创建内置4个线程的线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 2. 向线程池提交任务
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是一个任务....");
            }
        });
    }
}

其中使用步骤如下:

  1. 创建Executors线程池对象
    • Executors.newSingleThreadPool:创建带有单个线程的线程池
    • Executors.newFixedThreadPool:创建指定线程数目的线程池
    • Executors.newCachedThreadPool:创建可以动态扩容的线程池
    • Executors.newScheduledThreadPool:创建延时执行功能的线程池
  2. 得到ExecutorService对象
  3. 通过ExecutorService.submit可以注册一个任务到线程池中

1.4 模拟实现线程池

public class MyThreadPoolExecutor {
    private BlockingQueue<Runnable> blockingQueue;

    public MyThreadPoolExecutor(int nThreads) {
        this.blockingQueue = new ArrayBlockingQueue<>(1000);
        for (int i = 0; i < nThreads; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    // TODO:
                    while (true) {
                        // 从阻塞队列中获取任务并执行
                        try {
                            Runnable top = blockingQueue.take();
                            top.run();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            t.start();
        }
    }

    /**
     * 添加任务
     * @param runnable
     */
    public void submit(Runnable runnable) {
        try {
            this.blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上述代码中我们模拟实现了一个线程池对象

实现步骤如下:

  1. 内置属性BlockingQueue<Runnable>阻塞队列用来缓存任务
  2. 构造方法我们创建了nThread数量的线程,并且执行run方法,我们让每个线程不断扫描阻塞队列,取出队头元素并执行其中的方法,由于阻塞队列BlockingQueue是线程安全并且带有阻塞功能,所以我们无需手动加锁,也无需判断队列是否为空
  3. submit方法是用来提交任务的,该方法将提交的任务放入阻塞队列中,由于阻塞对象是线程安全并且带有阻塞功能的,所以这里也无需担心队列满的情况,也不用手动加锁。

测试上述代码:

public class MyThreadPoolExecutorTest {
    public static void main(String[] args) {
        // 1. 创建线程池对象
        MyThreadPoolExecutor myThreadPoolExecutor = new MyThreadPoolExecutor(4);
        // 2. 添加任务
        myThreadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是一个任务...");
            }
        });
    }
}

可以正常运行!这样我们就模拟实现了一个线程池对象

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值