多线程编程9——线程池

一、为什么要引入线程池?

虽然对比进程,线程已经很轻量了,创建销毁调度线程都更高效。但是随着并发程度的提高,我们对于性能要求的标准也越来越高,当我们需要频繁创建销毁调度线程时,就发现线程也没有那么轻量。于是就引入了线程池,来进一步提高效率。

二、线程池的理论知识

事先把需要的线程创建好,放到“线程池”中,后面使用的时候,直接从池里取;用完了,直接放回池中。从池中取和放回池中,这两个操作,比创建销毁线程更高效。

为什么呢?

因为从池中取和放回池中,是程序员自己的代码就可以实现的,想干啥,怎么干,都由程序员自己决定的,是用户态。

而创建和销毁线程,是由操作系统内核完成的,内核只会给我们提供一些API(也就是系统调用),我们只能够通过系统调用,让内核去完成创建或销毁线程,但我们并不清楚内核都要做哪些工作,身上背负着哪些任务,(内核并不是只给一个应用程序服务),什么时候才能帮我们去创建或销毁线程,整个过程是不可控的。

用户态比内核态更高效,所以使用线程池更高效。

三、线程池的使用

Java标准库中,提供了现成的线程池,可以直接使用。

这里使用到了“工厂模式”这种设计模式。

像 newFixedThreadPoll()这样的方法,称为“工厂方法”,提供这个工厂方法的类(Executors),称为“工厂类”。 工厂方法一般都是普通的静态方法,使用工厂类的工厂方法,来代替构造方法创建对象。(相当于把new对象的代码,放到工厂方法里了,我们只需要调用工厂方法,就能直接构造出一个对象来)

工厂类Executors 提供的线程池有很多种:

ExecutorService pool1 = Executors.newFixedThreadPool(10); 构造出固定线程数的线程池
ExecutorService pool2 = Executors.newCachedThreadPool(); 线程数量是动态变化的(如果任务多了,就多搞几个线程;如果任务少了,就少搞几个线程)
ExecutorService pool3 = Executors.newSingleThreadExecutor(); 线程池里只有一个线程
ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(10); 类似于定时器,让任务延时执行。

我们用 Executors.newFixedThreadPool(10)来举例。

此处是通过工厂方法创建了一个10个线程的线程池对象(线程池里已经有10个线程了,这些线程都是前台线程),然后我们就可以随时安排这些线程去干活了。

通过线程池提供的 submit方法,可以给线程池提交若干个任务(往线程池的任务队列里放任务)

于是,线程池里的每个线程,就会自己从任务队列中取走一个任务去执行,执行完,再立即去取下一个任务去执行。

代码如下:

public static void main(String[] args) {
        //构造了一个10个线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //通过submit,往线程池的任务队列里放100个任务
        for (int i = 0; i < 100; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    //往任务队列中放的的任务
                    System.out.println("hello "+n);
                }
            });
        }
    }

所以,使用Java线程的线程池时,只要通过submit往线程池的任务队列中指定任务就行啦,是不是非常方便呢。

四、实现一个线程池

实现固定数量线程的线程池

一个线程池里面至少要有2个大的部分

(1)阻塞队列:用来去保存任务

(2)若干个工作线程,每个线程的活:循环(从阻塞队列中取出任务,去执行)

提供一个submit方法,调用submit方法可以往任务队列中放任务。

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    //任务队列
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    //线程数
    public MyThreadPool(int n){
        //创建n个线程,每个线程都会不停从任务队列里拿任务去执行
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                while(true){
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
    //往任务队列里放任务
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}


public class ThreadDemo8 {
    public static void main(String[] args) {
        MyThreadPool myThreadPool = new MyThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int n = i;
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello "+n);
                }
            });
        }
    }
}

五、认识  ThreadPoolExecutor类 中构造方法的参数

上面介绍的工厂类Executors提供的这些线程池,本质上都是通过包装 ThreadPoolExecutor来实现出来的。只是因为ThreadPoolExecutor这个线程池用起来有些麻烦,所以才提供了工厂类,让我们用起来比较简单。ThreadPoolExecutor用起来麻烦的原因主要是因为它提供的功能更强大。所以也需要我们对它的构造方法进行掌握。

ThreadPoolExecutor这个类也在 java.util.concurrent包底下,这个包里放的很多类都是和并发编程(多线程编程)密切相关的。

corePoolSize:核心线程数

maximumPoolSize:最大线程数

keepAliveTime 和 unit 描述了线程 可以闲着不干活 的最大时间

workQueue:线程池的任务队列

threadFactory:用于创建线程

handler:描述了线程池的“拒绝策略”

ThreadPoolExecutor 相当于把里面的线程分为2类:一类是公司的正式员工,一类是临时工。正式员工就相当于核心线程。

核心线程数就是正式员工的数目,最大线程数就是正式员工+临时工的数目

如果任务多,就可以多招一些临时工,多搞一些线程。但是一个程序的任务不一定始终都很多,任务少时,有些临时工就闲着了,就不需要这么多临时工了。就需要动态调节线程的数目,对现有的一些线程进行淘汰。

keepAliveTime 和 unit就描述了临时工 可以闲着不干活 的最大时间,unit是时间单位

workQueue,是线程池的任务队列,此处使用阻塞队列。如果队列为空,执行出队列操作就会阻塞,直到队列里又有活了(队列不为空)。如果队列满了,往队列里添加任务也会阻塞,阻塞到队列里的任务被线程拿走执行了(队列不满)

threadFactory,用于创建线程,线程池里是需要创建线程的

handler,描述了线程池的“拒绝策略”,是一个特殊的对象,描述了当线程任务队列满了时,如果继续添加任务会有啥样的行为。标准库提供了四个拒绝策略。

如果任务队列满了,第一种,直接抛异常;第二种 ,多出来的任务,谁加的,谁负责执行。第三种,丢弃最早的任务。第四种,丢弃最新的任务。

六、实际开发中,线程池的线程数,我们到底应该设置成多少合适呢?

不同的程序。特点不同,此时要设置的线程数也是不同的。

有两个极端情况:

第一种,CPU密集型。每个线程执行的任务都需要疯狂使用CPU(如:进行一系列算术运算),此时线程池的线程数,最多也不应该超过CPU核数。设置更多也没有,没有CPU给他用了。

第二种,IO密集型。每个线程要干的工作就是等待IO(如:读写硬盘,读写网卡,等待用户输入),此时这样的线程处于阻塞状态,不参与CPU的调度。这个时候你想搞多少线程都可以。

然而,这两种只是理想化的模型,真实的程序,往往一部分要用CPU,一部分要等待IO,具体几成工作量是用CPU,几成工作量是用IO,都是不确定的。所以线程池的线程数,应该设置成多少也是不确定的。

所以,确定线程数,一般通过测试/实验的方式去确定。

  • 35
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于CPU多核编程——线程池开发方面的问题,我可以向您提供一些建议和思路。 线程池是一种常用的并发编程技术,其主要作用是在多核CPU的情况下,实现任务的并行执行,提高程序的性能。在实际开发中,线程池通常都采用生产者-消费者模式进行开发。 具体来说,线程池中有一个任务队列,负责存放待执行的任务。线程池内部有一些工作线程,它们会不断地从任务队列中取出任务进行执行。如果任务队列为空,那么工作线程会被挂起,等待新的任务到来。 在开发线程池时,关键是要实现线程的管理和同步。具体工作包括: 1. 配置线程池中工作线程的数量。这个数量通常要根据CPU的核心数和任务的类型来决定。如果任务是I/O密集型,那么工作线程数量应该设置得比较大;如果任务是计算密集型,那么工作线程数量可以适当减少。 2. 实现任务队列。这个队列可以采用多种数据结构来实现,比如数组、链表、堆等。对于多线程的情况,需要考虑线程之间的同步问题,防止数据竞争。 3. 实现工作线程。这些线程需要不断地从任务队列中取出任务进行执行,并且在没有任务时进入等待状态。同时,为了保证线程之间的公平性,需要采用一些调度算法,比如轮询、优先级队列等。 4. 实现任务接口。这些接口需要包括任务的执行函数以及相关的参数,以支持不同类型的任务。 总体来说,线程池的开发比较复杂,需要在多线程的情况下考虑到各种同步和调度问题。但是一旦实现成功,线程池可以大大提高程序的运行效率和响应速度。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值