【多线程】线程池

目录

一.池的概念

二.线程池的概念

三.标准库中的线程池

四.模拟线程池


一.池的概念

是一种资源复用和管理技术,预先创建并维护一组可以重复利用的资源,减少重复创建和销毁的开销,达到节省资源的目的

  1. 资源复用:重复利用已分配的资源,减少重复创建和销毁的开销
  2. 资源管控:限制资源总量(如果资源太多,容易发生线程安全问题)

二.线程池的概念

在最开始使用进程的时候,由于进程 的创建和销毁开销太大,于是引入了线程的概念(进程的更精细划分),虽然线程的开销很小,但是在频繁的创建和释放情况下,线程的开销也不得不重视起来,针对这个问题进行了优化。

  • 协程 / 纤程:轻量化线程,对线程的更精细划分

一个代码中,创建出上千个线程,可能会卡死,如果是上千个协程,只能说还不够

  • 线程池

将要使用的线程创建好,使用完后,将线程放入线程池中,下一次使用的时候,直接从线程池中取,不需要创建和销毁

并且在池子中获取线程,属于纯用户态操作(可控),由系统来创建,属于内核态操作(不可控),所以从线程池中获取线程 比从系统中申请线程更高效


三.标准库中的线程池

在java中,线程池的实现基于 java.util.concurrent 包中的 ThreadPoolExecutor

ThreadPoolExecutor类

maximumPoolSize (最大线程数)=核心线程数+空闲线程数

BlockingQueue<Runnable> workQueue (工作队列):这里使用Runnable来描述任务,我们也可以改进,使用PriorityBlockingQueue,带有优先级的阻塞队列,控制任务的执行顺序

ThreadFactory threadFactory (线程工程):线程池提供定制化线程的能力

拒绝策略!!!

线程池中,有一个阻塞队列,能够容纳的元素有上限,如果队列满了,还要继续添加任务,线程池会有四种解决办法

策略 行为 适用场景
AbortPolicy(默认策略) 抛异常 关键任务,需明确失败反馈
CallerRunsPolicy 提交者线程执行任务 缓解临时负载压力
DiscardPolicy 静默丢弃 非关键任务(如日志)
DiscardOldestPolicy 丢弃队列最旧任务并重试提交 实时性优先

AbortPolicy(默认策略)

  • 抛出异常,导致新任务和旧任务都不执行
  • 需要手动处理异常

CallerRunsPolicy

  • 新的任务由添加任务的线程去执行(新的任务会执行,不是由线程池中的线程去执行,而是由调用者执行)
  • 减缓任务提交速度,避免线程池过载,但是可能导致阻塞提交任务的线程。

DiscardPolicy

  • 丢弃最新的任务(新的任务直接销毁了,调用的线程和线程池都不执行)
  • 无异常处理负担,但是任务丢失不易察觉。

DiscardOldestPolicy

  • 丢弃最老的任务,去执行新的任务
  • 可能丢弃重要旧任务。

在我们的日常使用中,由于ThreadPoolExecutor类使用起来太复杂,标准库中提供了另一个Executors工厂类对ThreadPoolExecutor类进行封装并且设计好了不同的参数

下面是常用的方法:

|--------------------------------------|----------------|
| 方法 | 说明 |
| newScheduleThreadExecutor() | 创建定时器线程,延时执行任务 |
| newSingleThreadExecutor() | 只包含单个线程的线程池 |
| newCachedThreadExecutor() | 线程数目能够动态扩容 |
| newFixedThreadExecutor(int nThreads) | 创建固定大小的线程池 |

无论是ThreadPoolExecutor类还是Executor类都是使用submit方法添加任务

java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class Demo_7 {
    public static void main(String[] args) {

        ExecutorService service = Executors.newFixedThreadPool(3);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("111");
            }
        });
    }
}

在创建线程池的时候,会牵扯一个问题:线程池中线程的数量设置多少合适?

没有固定的答案,不同的程序,能够设定的线程数量是不同的

  1. 如果在一个进程中,所有的线程都是cpu密集型,那么每个线程的工作都在cpu上面,那么建议线程数目不要超过N(cpu核心数)
  2. 如果在一个进程中,所有的线程都是IO密集型,那么每个线程的工作都在等待IO,那么建议线程数目远远超过超过N(cpu核心数)

由于在实际开发中,一部分是cpu,另一部分IO,最好的办法是根据试验和测试找出合适的线程数

在不同的线程数目下,参考在总的时间的开销和系统资源的开销中,找到合适的量

四.模拟线程池

实现一个线程池,我们要明确以下步骤:

  1. 为了保证创建线程池就可以执行任务,将线程创建在构造方法中(只要创建出实例就会启动线程,执行任务)
  2. 构造方法中指定线程池中使用多少个线程
  3. 使用阻塞队列创建任务队列,当队列中没有任务的时候,可以实现阻塞的功能
  4. 提供一个submit方法,可以将任务添加进入任务队列中

主要步骤:

  1. 在构造方法中,使用for循环创建出指定的线程数,每个线程都进入while死循环,一直执行任务,如果队列中没有任务,则会进入take()方法**(带有阻塞功能)**
  2. 使用submit()方法将任务存放进入队列中
java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class MyThreadPoolExecutor{
//    存放线程
    List<Thread> list = new ArrayList<>();
//    存放任务
    BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
    MyThreadPoolExecutor(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                //线程具体的工作
                while(true){
                    try {
                        //取出任务
                        //使用take方法,可以进入阻塞
                        Runnable x =  queue.take();
                        //执行任务
                        x.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            //将每一个线程都启动起来
            t.start();
            list.add(t);
        }
    }

    //添加任务
    public void submit(Runnable runnable) throws InterruptedException {
        //会进行唤醒操作
        queue.put(runnable);
    }
}
public class Demo_8 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(9);

        for (int i = 0; i < 1000; i++) {
            //此处i是变量不能打印出,需要变成常量
            int n = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名:"+Thread.currentThread().getName()+"打印:"+n);
                }
            });
        }
    }
}

点赞的宝子今晚自动触发「躺赢锦鲤」buff!

相关推荐
松韬2 分钟前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
-代号95276 分钟前
【JavaScript】十三、事件监听与事件类型
开发语言·javascript·ecmascript
绝顶少年14 分钟前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端
心灵宝贝15 分钟前
Tomcat 部署 Jenkins.war 详细教程(含常见问题解决)
java·tomcat·jenkins
天上掉下来个程小白17 分钟前
Redis-14.在Java中操作Redis-Spring Data Redis使用方式-操作列表类型的数据
java·redis·spring·springboot·苍穹外卖
ゞ 正在缓冲99%…25 分钟前
leetcode22.括号生成
java·算法·leetcode·回溯
写代码的小王吧28 分钟前
【Java可执行命令】(十)JAR文件签名工具 jarsigner:通过数字签名及验证保证代码信任与安全,深入解析 Java的 jarsigner命令~
java·开发语言·网络·安全·web安全·网络安全·jar
小卡皮巴拉36 分钟前
【力扣刷题实战】矩阵区域和
开发语言·c++·算法·leetcode·前缀和·矩阵
伊成42 分钟前
Springboot整合Mybatis+Maven+Thymeleaf学生成绩管理系统
java·maven·mybatis·springboot·学生成绩管理系统
努力搬砖的咸鱼1 小时前
Qt中的数据解析--XML与JSON处理全攻略
xml·开发语言·qt·json