Java并发12:线程池—治理线程的法宝

102 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情

学习MOOC视频记录的笔记

1.线程池的自我介绍

1.1 线程池的重要性

什么是“池”

  • 软件中的“池”,可以理解为计划经济 (资源是有限的)

软件设计中有很多池化的例子:数据库连接池、线程池

池化的好处:复用线程;控制资源总量

如果不使用线程池,每个任务都新开一个线程处理

  • 一个线程
  • for循环创建线程
  • 当任务数量上升到1000?

这样开销太大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题。

1.2 为什么要使用线程池

  • 问题一:反复创建线程开销大
  • 问题二:过多的线程会占用太多内存

解决以上两个问题的思路

  • 用少量的线程——避免内存占用过多
  • 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗

1.3 线程池的好处

  • 加快响应速度 (不再需要反复创建和收回线程)
  • 合理利用CPU和内存 (CPU和内存都不是无限的,灵活调整数量)
  • 统一管理 (统一管理资源)

1.4 线程池适合应用的场合

  • 服务器接受到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理

2.创建和停止线程池

2.1 线程池构造函数的参数

参数名类型含义
corePoolSizeint核心线程数,详解见下文
maxPoolSizeint最大线程数,详解见下文
keepAliveTimelong保持存活时间
workQueueBlockingQueue任务存储队列
threadFactoryThreadFactory当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
HandlerRejectedExecutionHandler由于线程池无法接受你所提交的任务的拒绝策略
  • corePoolSize 指的是核心线程数:线程池在完成初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,再创建新线程去执行任务 【一直存活】
  • 线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,这就是最大量maxPoolSize 【任务数量不均匀是常态,更多线程的上限】

corePoolSize和maxPoolSize

添加线程规则:

  1. 如果线程数小于 corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务
  2. 如果线程数等于(或大于)corePoolSize 但少于 maximumPoolSize,则将任务放入队列
  3. 如果队列已满,并且线程数小于 maxPoolSize,则创建一个新线程来运行任务
  4. 如果队列已满,并且线程数大于或等于 maxPoolSize,则拒绝该任务

线程池添加线程规则

是否需要增加线程的判断顺序是:

  • corePoolSize
  • workQueue
  • maxPoolSize

比喻:烧烤店的桌子

真实案例:线程池:核心池大小为5,最大池大小为10,队列为100。

因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100。当队列已满时,将创建最新的线程 maxPoolSize,最多到10个线程,如果再来任务,就拒绝。

增减线程的特点

  1. 通过设置 corePoolSizemaximumPoolSize 相同,就可以创建固定大小的线程池。
  2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
  3. 通过设置 maximumPoolSize 为很高的值,例如 Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务。
  4. 是只有在队列填满时才创建多于 corePoolSize 的线程,所以如果你使用的是无界队列(例如 LinkedBlockingQueue),那么线程数就不会超过 corePoolSize。 【队列没有满】

KeepAliveTime含义:

  • 如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过 keepAliveTime,它们就会被终止

ThreadFactory 用来创建线程

  • 新的线程是由 ThreadFactory 创建的,默认使用 Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的 NORM PRIORITY 优先级并且都不是守护线程。如果自己指定 ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                poolNumber.getAndIncrement() +
                "-thread-";
    }
 
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                namePrefix + threadNumber.getAndIncrement(),
                0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

工作队列 workQueue

有3种最常见的队列类型:

  1. 直接交接:SynchronousQueue 【队列不缓冲】
  2. 无界队列:LinkedBlockingQueue 【不会被塞满,防止流量突增,可能造成OOM】
  3. 有界的队列:ArrayBlockingQueue 【队列满了就创建新线程】

2.2 线程池应该手动创建还是自动创建

  • 手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险
  • 让我们来看看自动创建线程池(也就是直接调用JDK封装好的构造函数)可能带来哪些问题

2.2.1 newFixedThreadPool

public class FixedThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            executorService.submit(new Task());
        }
    }
}
 
class Task implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

可见执行任务的始终是4个线程:

pool-1-thread-2
pool-1-thread-4
pool-1-thread-1
pool-1-thread-2
pool-1-thread-4

源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
    // corePoolSize = maxPoolSize = 4
    // 无界队列
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

由于传进去的 LinkedBlockingQueue 是没有容量上限的所以当请求数越来越多,并且无法及时处理完毕的时候也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致OOM。

/**
* -Xmx8m -Xms8m
*/
public class FixedThreadPoolOOM {
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
 
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executorService.execute(new SubThread());
        }
    }
}
 
class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

OOM

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at threadpool.FixedThreadPoolOOM.main(FixedThreadPoolOOM.java:14)

2.2.2 SingleThreadExecutor

public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}

始终只有一个线程执行任务:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

源码:

public static ExecutorService newSingleThreadExecutor() {
    return new Executors.FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

可以看出,这里和刚才的 newFixedThreadPool 的原理基本一样,只不过把线程数直接设置成了1,所以这也会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存

2.2.3 CachedThreadPool

  • 可缓存线程池
  • 特点:无界线程池,具有自动回收多余线程的功能

CachedThreadPool

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }
}

源码:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}

这里的弊端在于第二个参数 maximumPoolSize 被设置为了 Integer.MAX_VALUE,这可能会创建数量非常多的线程甚至导致OOM

2.2.4 newScheduledThreadPool

支持定时及周期性任务执行的线程池

public class ScheduledThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        // 延迟5s执行
        // threadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
        // 以固定速率执行: 最开始延时1s执行,每隔3s执行一次
        threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
 
    }
}

正确的创建线程池的方法

  • 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等

2.3 线程池里的线程数量设定为多少比较合适?

  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。 【线程已经在满负荷工作了】
  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于CPU核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法: 线程数=CPU核心数*(1+平均等待时间/平均工作时间)

2.4 停止线程池的正确方法

  • shutdown 关闭线程池,但是运行了这个方法之后并不一定会停止,这个方法只是初始化整个关闭过程,因为线程池执行到一半的时候会存在正在执行的任务,不是说停就停的,当执行了这个方法之后,线程池就知道了想让其停止,线程池为了优雅起见,会将正在执行的任务以及队列中等待的任务都执行完毕之后再关闭。往后如果有新的任务提交就会被拒绝。
  • isShutDown 判断线程池是否进入停止状态了
  • isTerminated 判断线程是否完全终止了
  • awaitTermination 不用来停止线程,在等待的时间内,如果线程都执行完毕了,会返回一个布尔值true,没有执行完毕会返回false。起到的主要作用是检测而不是关闭。这个方法在返回之前是阻塞的,只有三种情况会返回:1.所有任务都执行完毕了;2.等待的时间到了;3.等待过程中被中断了
  • shutdownNow 暴力,立刻关闭线程池。用中断信号触发正在执行的线程。正在队列中等待的任务会直接返回。
public class ShutDown {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        List<Runnable> runnableList = executorService.shutdownNow();
    }
 
    private static void method2() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        executorService.shutdown();
        // 已经过去了3秒,但是发现并没有完全终止  --->  false
        // boolean b = executorService.awaitTermination(3L, TimeUnit.SECONDS);
        // 7秒之内可以运行完毕,返回true
        // 这个方法是阻塞的
        boolean b = executorService.awaitTermination(7L, TimeUnit.SECONDS);
        System.out.println("b = " + b);
    }
 
    private static void method1() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            executorService.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        System.out.println(executorService.isShutdown());  // false
        // 不会立刻停止,而是会将存量的任务执行完毕
        executorService.shutdown();
        System.out.println(executorService.isShutdown());  // true
        System.out.println(executorService.isTerminated());  // false 还有很多线程没有执行完
        Thread.sleep(10000);
        System.out.println(executorService.isTerminated());  // true 所有任务执行完毕
        // 已经停止了新任务无法提交
        // executorService.execute(new ShutDownTask());
    }
}
 
class ShutDownTask implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "被中断了");
            // e.printStackTrace();
        }
    }
}

3.常见线程池的特点和用法

FixedThreadPool:固定数量的线程池

始终是一定数量的线程来执行任务,不会超出指定的范围

线程池结构

CachedThreadPool

  • 可缓存线程池
  • 特点:具有自动回收多余线程的功能

ScheduledThreadPool

  • 支持定时及周期性任务执行的线程池

SingleThreadExecutor

  • 单线程的线程池:它只会用唯一的工作线程来执行任务
  • 它的原理和 FixedThreadPool 是一样的,但是此时的线程数量被设置为了1

以上4种线程池的构造函数的参数

ParameterFixedThreadPoolCachedThreadPoolScheduledThreadPoolSingleThreadPool
corePoolSizeconstructor-arg0constructor-arg1
maxPoolSizesame as corePoolSizeInteger.MAX_VALUEInteger.MAX_VALUE1
keepAliveTime0 seconds60 seconds0 seconds0 seconds

阻塞队列分析

  • FixedThreadPoolSingleThreadExecutorQueueLinkedBlockingQueue

满足线程池功能,线程数量不能再往上膨胀了,只能使用一个无界队列来帮助存储任务,新来的任务数量没法估计,只能在自身上做文章。

  • CachedThreadPool使用的Queue是SynchronousQueue?

SynchronousQueue内部是不存储的,因为这种线程池是不需要存储的,任务过来了直接交给新的线程去处理,并且新线程的数量是不受限制的。

  • ScheduledThreadPool来说,它使用的是延迟队列DelayedWorkQueue

将任务根据时间先后做延迟,符号使用场景

workStealingPool是JDK1.8加入的

  • 这个线程池和之前的都有很大不同
  • 子任务 【任务可以切割产生子任务:树的遍历;处理矩阵,4个小矩阵,层层往下分】
  • 窃取 【每个线程之间会合作,任务会放到每个线程各自的任务队列中,其他线程也可能会帮助执行】

注意点:

  • 为了提高效率,任务最好不要加锁
  • 不保证执行顺序
  • 适用场景有限

4.任务太多,怎么拒绝?

拒绝时机

  1. Executor 关闭时,提交新任务被拒绝
  2. 以及当 Executor 对最大线程和工作队列容量使用有限边界并且已经饱和

线程池拒绝任务

4种拒绝策略

  • AbortPolicy:直接抛出一个异常,没有提交成功
  • DiscardPolicy:默默丢弃,不会得到通知
  • DiscardOldestPolicy:丢弃最老的,存在时间最久的被丢掉,以便来存放最新的任务
  • CallerRunsPolicy:谁提交的任务就由谁去执行(你说明天上线那你来试试看呀)。好处:避免业务损失;负反馈,减慢提交的速度,给了线程池缓冲的时间

5.钩子方法,给线程池加点料

  • 每个任务执行前后

  • 日志、统计

  • 代码演示

/**
* 可暂停线程池
* 演示每个任务执行前后放钩子函数
*/
public class PauseableThreadPool extends ThreadPoolExecutor {
 
    private final ReentrantLock lock = new ReentrantLock();
    private Condition unpaused = lock.newCondition();
    private boolean isPaused;
 
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
 
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }
 
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }
 
    public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
 
    // 每次执行任何一个任务之前都会调用这个函数来检查是否被标记为可暂停了
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        lock.lock();
        try {
            // 让当前这个线程暂停不再执行了
            while (isPaused) {
                unpaused.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
 
    private void pause() {
        lock.lock();
        try {
            isPaused = true;
        } finally {
            lock.unlock();
        }
    }
 
    // 恢复
    public void resume() {
        lock.lock();
        try {
            isPaused = false;
            unpaused.signalAll();
        } finally {
            lock.unlock();
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 20L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("我被执行");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 10000; i++) {
            pauseableThreadPool.execute(runnable);
        }
        Thread.sleep(1500);
        // 执行一段之间之后暂停线程
        pauseableThreadPool.pause();
        System.out.println("线程池被暂停了");
        Thread.sleep(1500);
        pauseableThreadPool.resume();
        System.out.println("线程池被恢复了");
 
    }
}

6.实现原理、源码分析

线程池组成部分

  • 线程池管理器
  • 工作线程
  • 任务列队
  • 任务接口(Task)

线程池结构

Executor家族?

线程池、ThreadPoolExecutor、ExecutorService、Executor、.Executors等这么多和线程池相关的类,大家都是什么关系?

哪个是线程池?

Executor:

public interface Executor {
    void execute(Runnable command);
}

ExecutorService

public interface ExecutorService extends Executor {
 
    void shutdown();
 
    List<Runnable> shutdownNow();
 
    boolean isShutdown();
 
    boolean isTerminated();
 
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;
 
    <T> Future<T> submit(Callable<T> task);
 
    <T> Future<T> submit(Runnable task, T result);
 
    Future<?> submit(Runnable task);
 
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;
 
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;
 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;
 
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
 

Executors 工具类,帮助快速创建线程池

线程池实现任务复用的原理

  • 相同线程执行不同任务 不需要重复启动线程,调用新任务的run方法,将一系列任务穿起来执行
  • 源码分析
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
 
    int c = ctl.get();
    // 当前线程数小于核心线程
    if (workerCountOf(c) < corePoolSize) {
        // 添加一个核心工作线程
        // command就是即将被线程执行的任务,与corePoolSize或者maxPoolSize比较
        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);
}

实现复用的逻辑,线程可以复用并且相同的线程可以执行不同的任务

final void runWorker(ThreadPoolExecutor.Worker w) {
    Thread wt = Thread.currentThread();
    // 拿到一个任务
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 任务不为空或者拿到的任务不为空,就去执行这个方法
        // 依次取到所有待执行的任务
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行任务
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

7.使用线程池的注意点

线程池状态

  • RUNNING: 接受新任务并处理排队任务

  • SHUTDOWN: 不接受新任务,但处理排队任务

  • STOP: 不接受新任务,也不处理排队任务,并中断正在进行的任务

  • TIDYING: 中文是整洁,理解了中文就容易理解这个状态了:所有任务都已终止,workerCount为零时,线程会转换到TIDYING状态,并将运行terminate()钩子方法。

  • TERMINATED: terminate() 运行完成

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

使用线程池注意点

  • 避免任务堆积
  • 避免线程数过度增加
  • 排查线程泄漏