《实战Java高并发程序设计》笔记——线程池


一、线程池概念

1.1 为什么需要线程池

  • 线程的创建和关闭需要消耗时间和资源
  • 线程本身也需要占用内存空间,处理不当可能会导致OOM
  • 大量线程的回收会给GC带来很大压力,延长GC的停顿时间

1.2 池化思想

1、可以提前创建一个池,里面总是有一些活跃线程;
2、当程序需要线程时,不创建线程,而是从线程池中获取线程;
3、当程序使用完线程后,不关闭线程,而是向线程池归还线程。


二、JDK对线程池的支持

2.1 Executor框架结构图

在这里插入图片描述

2.2 Executor框架提供的线程池

  • public static ExecutorService newFixedThreadPool(int nThreads):返回一个固定线程数量的线程池
  • public static ExecutorService newSingleThreadExecutor():返回一个只有一个线程的线程池
  • public static ExecutorService newCachedThreadPool():返回一个根据实际情况调整线程数量的线程池
  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):返回一个支持定时或周期性执行任务的线程池,可以指定线程数量
  • public static ScheduledExecutorService newSingleThreadScheduledExecutor():返回一个支持定时或周期性执行任务的线程池,线程数量为1

三、线程池的核心实现

3.1 构造函数

无论是newFixedThreadPool(),还是newSingleThreadExecutor()或newCachedThreadPool(),其内部实现均使用了ThreadPoolExecutor类

  • 其最重要的构造函数如下所示:
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

3.2 核心参数

  • corePoolSize:指定线程池中核心线程数量
  • maximumPoolSize:指定线程池中最大线程数量
  • keepAliveTime:线程池的线程数量超过corePoolSize,多余线程的存活时间
  • unit:keepAliveTime的单位
  • workQueue:任务队列,存放被提交但未执行的任务
  • threadFactory:线程工厂,用于创建线程
  • handler:拒绝策略。当任务太多来不及处理时,如何拒绝任务

3.3 任务队列

workQueue是指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象

3.3.1 SynchronousQueue(直接提交的队列)

  • 没有容量;
  • 每一个插入操作都要等待对应的删除操作;每一个删除操作都要等待对应的插入操作;
  • 处理任务的流程:提交的任务不会被保存,而是将新任务提交给线程执行,若没有空闲的线程,则尝试创建新线程,若线程数量已经达到最大,则执行拒绝策略。因此,使用synchronousQueue队列时一般将maximumPoolSize设置的比较大,否则很容易执行拒绝策略。

3.3.2 ArrayBlockingQueue(有界的任务队列)

  • 使用此队列时,必须在创建时指定一个容量参数,表示队列的最大容量;
  • 处理任务的流程(如下如所示):

在这里插入图片描述

  • 有界队列,只有当任务队列满时,才可能将线程数提升到corePoolSize以上。

3.3.3 LinkedBlockingQueue(无界任务队列)

  • 无需指定大小
  • 处理任务的流程(如下如所示):

在这里插入图片描述

  • 若任务创建的速度远远大于任务处理的速度,会导致无界队列快速增长,知道耗尽系统内存。

3.3.4 PriorityBlockingQueue(优先任务队列)

  • 优先任务队列是带有执行优先级的队列。它通过PriorityBolckingQueue类实现,可以控制任务的执行先后顺序。
  • 有界队列(ArrayBlockingQueue)和无界队列(LinkedBlokcingQueue)都是使用先进先出的算法处理任务;而优先任务队列(PriorityBlockingQueue)按照的是任务优先级来处理任务(总是保证高优先级的任务先执行)

3.4 默认线程池的的问题

1、newFixedThreadPool、newSingleThreadEcecutor()的问题

  • 返回线程池的corePoolSize和maximumPoolSize大小相同;
  • 同时使用无界队列LinkedBlockingQueue存放任务;
  • 当任务提交速度大于处理速度时,该队列会一直增长,从而导致OOM。

2、newCachedThreadPool()的问题

  • 返回线程池的corePoolSize大小为0,maximumPoolSize大小为无穷;
  • 使用直接提交的队列SynchronousQueue;
  • 当提交的速度大于任务的处理速度,线程池会不断地创建新的线程,从而导致OOM。

3.5 核心调度代码

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    
    // 当前线程数<corePoolSize,创建新线程执行
    if (workerCountOf(c) < corePoolSize) {	// workerCountOf()取得当前线程池的线程总数
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 进入等待队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 进入等待队列失败,提交给线程池
    else if (!addWorker(command, false))
        // 线程数量达到最大,执行拒绝策略
        reject(command);
}

线程创建的时机:
在这里插入图片描述


四、拒绝策略

4.1 JDK内置拒绝策略

  • AbortPolicy策略:直接抛出异常,阻止系统正常工作
  • DiscardPolicy策略:直接丢弃无法处理的任务,同时不会产生任何提醒
  • DiscardOledestPolicy策略:丢弃最老(即队列首节点)的请求,并再次尝试提交当前任务
  • CallerRunsPolicy策略:只要线程池未关闭,就在调用者线程中运行当前被丢弃的任务。

4.2 自定义拒绝策略

  • JDK内置的策略均实现了RejectExecutionHandler接口,我们可以通过实现此接口来自定义拒绝策略,其接口定义如下所示
public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

4.3 代码示例

public class RejectThreadPoolDemo {
    public static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis() + ":Thread.ID:" + Thread.currentThread().getId());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 1、创建任务
        MyTask myTask = new MyTask();
        // 2、创建线程池
        ExecutorService pool = new ThreadPoolExecutor(
            5,      // 核心线程数
            5,  	// 最大线程数
            0L,     // 存活时间
            TimeUnit.MILLISECONDS,  				// 时间单位
            new LinkedBlockingQueue<Runnable>(10),  // 任务队列
            Executors.defaultThreadFactory(),   	// 线程工厂
            new RejectedExecutionHandler() {   		// 自定义拒绝策略
                @Override
                public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                    System.out.println(r.toString() + "  is discard!");
                }
            }
        );
        // 三、提交任务
        for (int i = 0; i < 100; i++) {
            pool.submit(myTask);
            Thread.sleep(10);
        }
    }
}

运行结果
在这里插入图片描述


五、ThreadFactory

自定义线程创建可以帮助我们

  • 跟踪线程池再何时创建了多少线程
  • 自定义线程名称
  • 自定义线程优先级
  • 将线程设置为守护线程
  • 。。。

代码示例:
将线程池创建的所有线程都设置为守护线程,当主线程退出后,会强制销毁线程池

public class ExtendThreadFactory {
    public static class MyTask implements Runnable {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis() + ":Thread.ID:" + Thread.currentThread().getId());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask myTask = new MyTask();
        ExecutorService pool = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>(),
                // 自定义线程工厂,将线程都设置为守护线程
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setDaemon(true);
                        System.out.println("Create " + t);
                        return t;
                    }
                });
        for (int i = 0; i < 5; i++) {
            pool.submit(myTask);
        }
        Thread.sleep(2000);
    }
}

运行结果:
在这里插入图片描述


六、扩展线程池

虽然JDK已经帮我们实现了高性能的线程池,但我们有时想要对线程池做一些拓展功能例如:监控每个任务的开始和结束时间或者其他自定义的增强功能,此时我们就可以对线程池进行扩展

6.1 相关方法

ThreadPoolExecutor是一个可以扩展的线程池,它提供了三个接口对线程池进行控制,分别用于

  • beforeExecute():记录一个任务的开始
  • afterExecute():记录一个任务的结束
  • terminated():记录整个线程池的退出

6.2 代码示例

public class ExtendThreadPool {

    // 任务
    public static class MyTask implements Runnable {
        public String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("正在执行:Thread.ID:" + Thread.currentThread().getId()
                               + ",Task Name:" + this.name);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 主程序
    public static void main(String[] args) throws InterruptedException {
        // 一、创建线程池
        ExecutorService pool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                                                      new LinkedBlockingQueue<Runnable>()) {
            // 任务开始前处理
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("准备执行:" + ((MyTask) r).name);
            }

            // 任务开始后处理
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("执行完成:" + ((MyTask) r).name);

            }

            // 整个线程池退出时的处理
            @Override
            protected void terminated() {
                System.out.println("线程池退出!");
            }
        };
        // 二、提交任务
        for (int i = 0; i < 5; i++) {
            MyTask task = new MyTask("TASK-" + i);
            pool.execute(task);
            Thread.sleep(10);
        }
        // 三、关闭线程池
        pool.shutdown();
    }
}

运行结果
在这里插入图片描述


七、优化线程池线程数量

  • 线程池的线程数量太少,可能无法发挥最大的性能;而线程数量太多,上下文切换所消耗的时间和资源太多。所以,合理定义线程池的线程数量是非常重要的
  • 经验公式
    Ncpu=CPU的数量
    Ucpu=目标CPU的使用率,0<=Ucpu<=1
    W/C=等待时间与计算时间的比率
    为保持处理器达到期望的使用率,最优的线程池线程数量大小为:
    Nthreads=Ncpu*Ucpu*(1+W/C)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值