线程池学习

创建线程

Java语言中,创建线程只需要new Thread()这么简单, 但是创建线程不是创建一个普通对象这么简单, 创建对象,知识在JVM堆内存中分配一块内存而已; 但是创建一个线程是需要调用操作系统内核的API, 然后为线程分配资源, 创建成本很高

创建一个线程是需要占用内存的,在JVM中创建线程除了要消耗内存, 还会给GC带来压力, 如果频繁的创建线程那么相对的GC的时候也需要回收对应的线程

线程是一个重量级的对象, 应该避免频繁创建和销毁.

线程池

线程池和一般的池化资源不同. 一般的池化资源, 在需要资源的时候调用acquire()申请资源, 用完之后调用release()释放资源.

Java线程池里面没有申请线程和释放线程的方法.

class XXXPool {
    // 申请资源
    XXX acquire() {
        
    }
    
    // 释放资源
    void release() {
        
    }
}

线程池是 生产者 - 消费者模式

如果线程池采用的是一般的池话资源的设计方法, 应该是下面的代码示例

我们初始化线程池之后, 调用acquire()方法获取线程, 传入一个Runnable对象来执行具体业务逻辑, 就像通过构造函数Threa(Runnable task)创建线程一样, 可是, Thread对象除了构造函数, 就不存在接收Runnable的公共方法了

// 采用一般池化资源的设计方法
class ThreadPool {
    // 获取空闲线程
    Thread acquire() {
        
    }
    // 释放线程
    void release() {
        
    }
}

// 期望的使用
ThreadPool pool = new ThreadPool();
Thread t = pool.acquire();
// 传入Runnable对象
t.execute(() -> {
    // 具体逻辑
    ...
})

所以, 线程池的设计, 没有采用一般池化资源的设计方法. 目前业界线程池的设计, 普遍采用的都是生产者-消费者模式. 线程池的使用方是生产者(生产Runnable传入线程池), 线程池本身是消费者(消费传入的Runnable)

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

/**
 * @author jiale.he
 * @date 2019/09/27
 * @email jiale.he@mail.hypers.com
 */
public class MyThreadPool2 {
    public static void main(String[] args) {
        // 使用示例
        // 创建有界阻塞队列
        LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
        ThreadPool pool = new ThreadPool(10, workQueue);

        pool.execute(() -> {
            System.out.println("hello");
        });
    }
}

class ThreadPool {
    // 利用阻塞队列实现生产者消费者模式
    BlockingQueue<Runnable> workQueue;
    // 保存内部工作线程
    List<WorkThread> threadList = new ArrayList<>();

    // 构造
    ThreadPool(int poolSize, BlockingQueue<Runnable> workQueue) {
        this.workQueue = workQueue;
        for (int i = 0; i < poolSize; i++) {
            WorkThread workThread = new WorkThread();
            workThread.start();
            threadList.add(workThread);
        }
    }

    // 提交任务
    void execute(Runnable task) {
        workQueue.add(task);
    }

    // 工作线程负责消费任务, 并执行任务
    class WorkThread extends Thread {
        @Override
        public void run() {
            while (true) {
                try {
                    Runnable task = workQueue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在MyThreadPool内部, 我们维护了一个阻塞队列workQueue和一组工作线程, 工作线程的个数由构造函数中的poolSize指定. 用户通过调用execute()方法来提交Runnable任务. execute()方法的内部实现仅仅只是将任务加入道workQueue中. MyThreadPool内部维护的工作线程会消费workQueue中的任务并执行任务, 相关代码就是while中的循环.

如何使用Java中的线程池

Java并发包里提供的线程池, 远比我们上面的示例代码强大得多, 也复杂得多. Java提供的线程池相关的工具类中, 最核心的是ThreadPoolExecutor

ThreadPoolExecutor的构造函数非常复杂,有7个参数

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 

corePoolSize: 表示线程池保有的最小线程数, 核心线程数, 期望保持的并发状态, 不是立即创建线程, 是类似懒加载

maximumPoolSize: 最大线程数, 允许超载, 虽然期望将并发状态保持在一定范围, 但是在任务过多时, 增加非核心线程来处理任务, 非核心线程=maximumPoolSize-corePoolSize

unit: 与keepAliveTime配合, 设置keepAliveTime的单位, 如: 毫秒, 秒

workQueue: 阻塞队列, 用来存储线程任务Runnable

KeepAliveTIme: 在没有任务时, 线程存活时间

threadFactory: 用来构建线程

handler: 当任务已满, 并且无法增加线程数时,或拒绝添加任务时, 所执行的策略
通过handler这个参数你可以自定义任务的拒绝策略, 如果线程池中所有的线程都在忙碌,并且工作队列也满了(有界阻塞队列)
那么此时提交任务, 线程池就会拒绝接受. ThreadPoolExecutor提供了一下四种策略.
- CallerRunsPolicy: 提交任务的线程自己去执行该任务
- AbortPolicy: 默认的拒绝策略, 会throw RejectedExecutionException
- DiscardPolicy: 直接丢弃, 没有任何异常抛出
- DiscardOldestPolicy: 丢掉最老的任务, 其实就是把工作队列中最早进去的任务丢弃,然后把最新的任务加入到队列

容易混淆的参数

corePoolSize

maximumPoolSize

workQueue

任务队列, 核心线程数, 最大线程数的逻辑关系

  1. 当线程数 < 核心线程数时, 创建线程
  2. 当线程数 > 核心线程数, 且任务队列未满时, 将任务放入任务队列
  3. 当线程数 > 核心线程数, 且任务队列已满
    • 如果线程数 < 最大线程数, 创建线程
    • 如果线程数 = 最大线程数, 调用拒绝策略处理程序(默认为: 抛出异常, 拒绝任务)

注意

  1. 因为ThreadPoolExecutor的构造函数实在复杂, 所以Java并发包里提供了线程池的静态工厂类Executors, 利用Executors可以快速创建线程池, 不过目前大厂的编码规范中不建议使用Executors.
  2. 不建议使用Executors的最重要原因是: Executors提供的很多方法默认使用的都是无解的LinkedBlockingQueue, 高负载情况下, 无解队列很容易导致OOM, 而OOM会导致所有请求都无法处理, 强烈建议使用有界队列
  3. 使用有界队列, 当任务过多时, 线程池会触发执行拒绝策略, 线程池的默认拒绝策略会throw RejectedExecutionException,这是个运行时异常,对于运行时异常编译器并不强制catch, 所以开发人员很容易忽略. 因此默认拒绝策略要谨慎使用. 如果线程池处理的任务很重要, 建议自定义自己的拒绝策略; 并且在实际工作中, 自定义的拒绝策略往往和降级策略配合使用
  4. 使用线程池, 还要注意异常处理的问题, 例如通过ThreadPoolExecutor对象的execute()方法提交任务时, 如果任务在执行的过程中出现运行时异常, 会导致执行任务的线程终止; 不过最致命的是虽然任务异常了, 但是你却获取不到任何通知, 这回让你误以为任务都执行得很正常. 虽然线程池提供了很多用于处理异常的方法, 但是最稳妥和简单的方法还是捕获所有异常并按需处理, 可以参考以下代码
try{
    // 业务逻辑
} catch (RuntimeException e) {
    // 按需处理
} catch (Throwable e) {
    // 按需处理
}

Java中的线程池详解

可以看到真正的实现类有

  1. ThreadPoolExecutor (1.5)
  2. ForkJoinPool (1.7)
  3. ScheduledThreadPoolExecutor (1.5)

主要谈一下 ThreadPoolExecutor 也是使用频率较高的一个实现

Executors提供的工厂方法

newCachedThreadPool()

创建一个可缓存的线程池. 如果线程池的大小超过了处理任务所需要的线程, 那么就会回收部分空闲(60S 不执行任务)的线程, 
当任务数增加时, 此线程又可以添加新线程来处理任务. 此线程池不会对线程池大小做限制, 线程池大小完全你来JVM能够创建的最大线程

newFiexedThreadPool()

创建固定大小的线程池. 每次提交一个任务就创建一个线程, 知道线程达到线程池的最大值, 线程池的大小一旦达到
最大值就会保持不变, 如果某个线程因为异常而结束, 那么线程池会补充一个新的线程.

newSingleThreadExecutor()

创建一个单线程的线程池, 这个线程池只有一个线程在工作, 也就是单线程串行执行所有任务. 如果这个唯一的线程因为异常结束, 
那么会有一个新的线程来替代他,. 此线程池保证所有任务的执行顺序按照任务的提交顺序执行.

newScheduledThreadPool()

创建一个大小无限的线程池, 此线程池支持定时一集周期性执行任务的需求

newSingleThreadScheduledExecutor()

创建一个单线程用于定时一集周期性执行任务的需求

newWorkStealingPool (1.8 ForkJoinPool)

创建一个工作窃取线程池

可以看到各种不同工厂方法中使用的线程池实现类最终只有三个, 对应关系如下

阿里开发规范为什么不允许Executor快速创建线程池

线程池不允许使用Executor去创建, 而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学明确线程池的运行规则, 规避资源耗尽的风险.

弊端如下:

  1. FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为Integer.MAX_VALUE, 可能会堆积大量的请求, 从而导致OOM
  2. CachedThreadPool: 允许创建线程数量为Integer.MAX_VALUE, 可能会创建大量的线程, 从而导致OOM

下面代码输出是什么?

/**
 * @author jiale.he
 * @date 2019/10/08
 * @email jiale.he@mail.hypers.com
 */
public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1, //corePoolSize
                100, //maximumPoolSize
                100, //keepAliveTime
                TimeUnit.SECONDS, //unit
                new LinkedBlockingDeque<>(100));//workQueue

        for (int i = 0; i < 5; i++) {
            final int taskIndex = i;
            executor.execute(() -> {
                System.out.println(taskIndex);
                try {
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}
答案是 0

最大线程数是100, 但是核心线程是1, 任务队列是100
需要的线程数是5

满足了 线程数 > 核心线程数, 任务队列未满
所以会将后续的任务加入阻塞队列, 第一个线程sleep(Integer.MAX_VALUE), 导致后续线程全部阻塞

本文是我在极客时间和网上查找相关资料的学习笔记总结, 只为记录自己的学习进度

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值