剑指JUC原理-13.线程池

  • 作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 系列专栏:Spring源码、JUC源码
  • 如果感觉博主的文章还不错的话,请三连支持一下博主哦
  • 博主正在努力完成2023计划中:源码溯源,一探究竟
  • 联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

文章目录

    • ThreadPoolExecutor
      • 线程池状态
      • 构造方法
      • newFixedThreadPool
      • newCachedThreadPool
      • newSingleThreadExecutor
      • 提交任务
        • Future机制原理
          • 回顾Runnable和Callable
          • Future机制原理
          • Future源码解析
      • 关闭线程池
        • shutdown流程
        • shutdownNow流程
        • 问题说明
          • 线程是如何中断的
          • 为什么修改线程池状态为shutdown以后线程池就不能接收新任务了
            • execute方法
            • addWorker方法
        • 源码比较
          • shutdown源码
          • shutdownNow源码
      • **异步模式之工作线程**
        • 定义
        • 饥饿
        • 创建多少线程池合适
          • CPU 密集型运算
          • I/O 密集型运算
      • 任务调度线程池
      • 正确处理执行任务异常
        • 方法1:主动捉异常
        • 方法2:使用 Future
      • 任务调度线程池模拟定时执行任务
      • Tomcat线程池
    • Fork/Join
      • 概念
      • 使用

ThreadPoolExecutor

剑指JUC原理-13.线程池_第1张图片

线程池状态

ThreadPoolExecutor 使用 int 的高 3 位(最高位1代表的是负数)来表示线程池状态,低 29 位表示线程数量

状态名 高三位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN 000 N Y 不会接收新任务,但会处理阻塞队列剩余任务
STOP 001 N N 会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING 010 任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED 011 终结状态

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN(调用了线程池的SHUTDOWN) > RUNNING(创建出来的初始状态)

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作
进行赋值

// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));

// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

构造方法

public ThreadPoolExecutor(int corePoolSize,
 			int maximumPoolSize,
 			long keepAliveTime,
 			TimeUnit unit,
 			BlockingQueue<Runnable> workQueue,
 			ThreadFactory threadFactory,
			RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目(核心线程 + 救急线程数)
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

工作方式:

剑指JUC原理-13.线程池_第2张图片

线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。

如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线
程来救急。(救急线程和核心线程的最大区别就是,它有生存时间,执行完了,过一段时间,没有新任务了,就销毁掉了,下次高峰期来了,才会再有)

如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它
著名框架也提供了实现

  • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
  • CallerRunsPolicy 让调用者运行任务
  • DiscardPolicy 放弃本次任务
  • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

剑指JUC原理-13.线程池_第3张图片

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。

其它优秀框架的实现(总结整理)

  • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
    便定位问题
  • Netty 的实现,是创建一个新线程来执行任务
  • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
  • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。

newFixedThreadPool

固定大小的线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
 	return new ThreadPoolExecutor(nThreads, nThreads,
 								0L, TimeUnit.MILLISECONDS,
 								new LinkedBlockingQueue<Runnable>());
}

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务

评价 适用于任务量已知,相对耗时的任务

newCachedThreadPool

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

特点

1.核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着

  • 全部都是救急线程(60s 后可以回收)
  • 救急线程可以无限创建

2.队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交
货)

SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
 	try {
 		log.debug("putting {} ", 1);
 		integers.put(1);
 		log.debug("{} putted...", 1);
 		
 		log.debug("putting...{} ", 2);
 		integers.put(2);
 		log.debug("{} putted...", 2);
 	} catch (InterruptedException e) {
 		e.printStackTrace();
 	}
},"t1").start();

sleep(1);

new Thread(() -> {
 	try {
 		log.debug("taking {}", 1);
 		integers.take();
 	} catch (InterruptedException e) {
 		e.printStackTrace();
 	}
},"t2").start();

sleep(1);

new Thread(() -> {
 	try {
 		log.debug("taking {}", 2);
 		integers.take();
 	} catch (InterruptedException e) {
 		e.printStackTrace();
 	}
},"t3").start();

输出

11:48:15.500 c.TestSynchronousQueue [t1] - putting 1 
11:48:16.500 c.TestSynchronousQueue [t2] - taking 1 
11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted... 
11:48:16.500 c.TestSynchronousQueue [t1] - putting...2 
11:48:17.502 c.TestSynchronousQueue [t3] - taking 2 
11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted... 

评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线
程。 适合任务数比较密集,但每个任务执行时间较短的情况

newSingleThreadExecutor

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

使用场景:

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程
也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

提交任务

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
 throws InterruptedException;

// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
 long timeout, TimeUnit unit)
 throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
 throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
 long timeout, TimeUnit unit)
 throws InterruptedException, ExecutionException, TimeoutException;


public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(1);

    }

    private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException {
        String result = pool.invokeAny(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(1000);
                    log.debug("end 1");
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(500);
                    log.debug("end 2");
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    log.debug("end 3");
                    return "3";
                }
        ));
        log.debug("{}", result);
    }

    private static void method2(ExecutorService pool) throws InterruptedException {
        List<Future<String>> futures = pool.invokeAll(Arrays.asList(
                () -> {
                    log.debug("begin");
                    Thread.sleep(1000);
                    return "1";
                },
                () -> {
                    log.debug("begin");
                    Thread.sleep(500);
                    return "2";
                },
                () -> {
                    log.debug("begin");
                    Thread.sleep(2000);
                    return "3";
                }
        ));

        futures.forEach( f ->  {
            try {
                log.debug("{}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
    }

    private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
        Future<String> future = pool.submit(() -> {
            log.debug("running");
            Thread.sleep(1000);
            return "ok";
        });

        log.debug("{}", future.get());
    }

通过以上的例子,可以获取到对应的结果,过程不做赘述,下面主要介绍Future为什么能获取到 线程的结果并返回,至于其它几个返回值,大同小异!

Future机制原理
回顾Runnable和Callable
  • Callable定义了call()方法,Runnale定义了run()方法。

  • call()方法可以抛出异常,run()方法无法抛出异常。

  • Callable有返回值,是泛型的,创建的时候传递进去,执行结束后返回。

  • Callable执行任务的时候可以通过FutureTask得到任务执行的状态。

Callable的call方法实际执行在Runnable的run方法中。Runnable实例对象需要Thread包装启动,Callable先通过FutureTask(本质还是Runnable)包装,再给Thread包装执行。

Future机制原理

Future就是对于具体的Runnable或Callable任务的执行结果进行取消,查询是否完成,获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

JDK内置的Future主要使用了Callable接口和FutureTask类。下面对源码进行解析:

Callable接口:

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

如何使用Callable:

一般情况下都是配合ExecutorService来使用的,在ExecutorService接口中声明了三个submit方法,用来生成Future对象,参数为Callable实例或Runnable实例。

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

继承关系:

public class FutureTask<V> implements RunnableFuture<V> {}
 
 
public interface RunnableFuture<V> extends Runnable, Future<V> { void run();}

关系明确: FutureTask类实现了RunnableFuture接口,RunnableFuture接口又继承了Runnable接口和Future接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,拥有Future接口提供的各种方法。

流程跑通: 通常把任务定义Callable接口的call方法内部,返回值为泛型,再生成一个FutureTask的对象,FutureTask构造方法内部参数封装着Callable实例,然后把这个对象当作一个Runnable,作为参数传递给Thread包装执行。

Future源码解析

Future接口提供的方法:

public interface Future<V> {
 
    //取消任务。参数:是否立即中断任务执行,或者等等任务结束
    boolean cancel(boolean mayInterruptIfRunning);
 
    //任务是否已经取消,若已取消,返回true
    boolean isCancelled();
 
    //任务是否已经完成。包括任务正常完成、抛出异常或被取消,都返回true
    boolean isDone();
 
    /*会一直阻塞等待任务执行结束,获得V类型的结果。InterruptedException: 线程被中断异常, ExecutionException: 任务执行异常,如果任务被取消,还会抛出CancellationException*/
    V get() throws InterruptedException, ExecutionException;
 
    /*参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException*/
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask源码解析:

构造方法:

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        //状态为NEW
        this.state = NEW;       // ensure visibility of callable
    }
 
public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

实际上,Callable = Runnable + result,看Executors.callable(runnable,result)的实现:

public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        //new了一个RunnableAdapter,返回Callable,说明RunnableAdapter实现了Callable
        return new RunnableAdapter<T>(task, result);
    }

利用RunnableAdapter适配器实现了将Runnable转为Callable,继续看RunnableAdapter类:

static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            //Runnable task执行了run()
            task.run();
            //返回了T result
            return result;
        }
    }

本质上还是Runnable去执行run方法,只是增加了result常量来接受返回的结果而已。

FutureTask的run方法:

public void run() {
        /*compareAndSwapObject(this, runnerOffset,]null, Thread.currentThread()))
         其中第一个参数为需要改变的对象,第二个为偏移量,第三个参数为期待的值,第四个为更新后的值。
        */
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //call()方法是由FutureTask调用的,说明call()不是异步执行的
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    //设置异常
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            //判断是否被中断
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

set方法:

protected void set(V v) {
           // NEW -> COMPLETING
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            //返回结果,也包括异常
            outcome = v;
            //COMPLETING -> NORMAL
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            //唤醒等待的线程
            finishCompletion();
        }
    }

将返回值和状态赋值回去。

report方法:

private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

get方法:

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //是否是未完成状态,是则等待
        if (s <= COMPLETING)
            //等待过程
            s = awaitDone(false, 0L);
        return report(s);
    }
 
    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (unit == null)
            throw new NullPointerException();
        int s = state;
        if (s <= COMPLETING &&
            (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }

run方法解析完成,使用get方法获取返回结果。

总结:Future只实现了异步,而没有实现回调,主线程get时会阻塞,可以轮询以便获取异步调用是否完成。

关闭线程池

shutdown流程
  • 修改线程池状态为SHUTDOWN

  • 不再接收新提交的任务

  • 中断线程池中空闲的线程

  • 第③步只是中断了空闲的线程,但正在执行的任务以及线程池任务队列中的任务会继续执行完毕

shutdownNow流程
  • 修改线程池状态为STOP

  • 不再接收任务提交

  • 尝试中断线程池中所有的线程(包括正在执行的线程)

  • 返回正在等待执行的任务列表 List

此时线程池中等待队列中的任务不会被执行,正在执行的任务也可能被终止(为什么是可能呢?因为如果正常执行的任务如果不响应中断,那么就不会被终止,直到任务执行完毕)

问题说明
线程是如何中断的

中断线程池中的线程的方法是通过调用 Thread.interrupt()方法来实现的,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt() 方法是无法中断当前的线程的(因为sleep、condition、await这些是响应中断的)。所以,shutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候是能立即退出的。

为什么修改线程池状态为shutdown以后线程池就不能接收新任务了

在向线程池提交任务的时候,会先检查线程池状态, 线程池状态为非关闭(或停止)时才能提交任务,这里已经将线程池状态修改为shutdown了,自然就不能接受新的任务提交了,可参考execute(Runnable command)逻辑和 addWorker逻辑)

execute方法
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
  
    int c = ctl.get();
    // 【 1 】、worker数量比核心线程数小,直接创建worker执行任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 【 2 】、worker数量超过核心线程数,任务直接进入队列。这里进入队列前先判断了线程池状态
    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);
    }
    //【 3 】、 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务(即:线程阻塞队列满了但线程池中的线程数没达到最大线程数,
    // 则新开启一个线程去执行该任务)。
    // 这儿有3点需要注意:
    // 1. 线程池不是运行状态时,addWorker内部会判断线程池状态
    // 2. addWorker第2个参数表示是否创建核心线程
    // 3. addWorker返回false,则说明任务执行失败,需要执行reject操作
    else if (!addWorker(command, false))
        reject(command);
}
addWorker方法
// 新建一个worker
w = new Worker(firstTask);
int rs = runStateOf(ctl.get());

// 这儿需要重新检查线程池状态,即线程池状态为shutdown以后不能再提交任务了
if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
  // 添加到工作线程集合中
  workers.add(w);
  int s = workers.size();
  // 更新largestPoolSize变量的值
  if (s > largestPoolSize)
    largestPoolSize = s;
  workerAdded = true;
}
源码比较
shutdown源码
public void shutdown() {
  final ReentrantLock mainLock = this.mainLock;
  // 获取线程池的锁
  mainLock.lock();
  try {
    // 检查关闭进入许可
    checkShutdownAccess();
    // 将线程池状态改为SHUTDOWN
    advanceRunState(SHUTDOWN);
    // 【中断线程池中空闲线程】,注意和下面的shutdownNow方法中的进行对比
    interruptIdleWorkers();
    // 留给定时任务线程池的钩子方法,这里没有实现,在定时任务线程池中有实现
    onShutdown(); // hook for ScheduledThreadPoolExecutor
  } finally {
    mainLock.unlock();
  }
  
  // ①、如果线程池状态为正在运行 或 已经是 TIDYING 状态以上了 或者  线程池状态为shutdown但是等待队列中还有任务,
  // 那么这个方法什么都不做,直接返回
  // ②、如何线程池中还有未被中断的线程,则这里会再次去中断他(并且利用中断传播 从等待队列中删除等待的worker)
  // ③、如果线程池的状态是shutdown,并且等待队列中已经没有任务了,那么此时会把线程池状态转换为 TIDYING,
  // 并唤醒所有调用awaitTermination()等待线程池关闭的线程
  tryTerminate();
}
shutdownNow源码
public List<Runnable> shutdownNow() {
  List<Runnable> tasks;
  // 获取线程池的锁
  final ReentrantLock mainLock = this.mainLock;
  mainLock.lock();
  try {
    // 检查关闭进入许可
    checkShutdownAccess();
    // 将线程池状态改为STOP
    advanceRunState(STOP);
    // 【中断所有线程】
    interruptWorkers();
    // 获取任务队列里未完成任务
    tasks = drainQueue();
  } finally {
    mainLock.unlock();
  }

  // ①、如果线程池状态为正在运行 或 已经是 TIDYING 状态以上了 或者  线程池状态为shutdown但是等待队列中还有任务,
  // 那么这个方法什么都不做,直接返回
  // ②、如何线程池中还有未被中断的线程,则这里会再次去中断他(并且利用中断传播 从等待队列中删除等待的worker)
  // ③、如果线程池的状态是shutdown,并且等待队列中已经没有任务了,那么此时会把线程池状态转换为 TIDYING,
  // 并唤醒所有调用awaitTermination()等待线程池关闭的线程
  tryTerminate();
  return tasks;
}

通过比较shutdown源码和shutdownNow的源码我们可以发现,这两个方法最大的不同在于中断线程的地方:

  • 首先,需要再次明确的一点是,中断线程并不是立即把这个线程停止,而是把这个线程的【中断状态】设置为true,表示有其他线程来中断过这个线程。

  • shutdown方法调用interruptIdleWorkers()方法中断的只是线程池中空闲的线程,那么也就说明线程池中正在工作的线程没有被中断,所以说正在工作的线程会继续执行完毕,并且正在工作的线程也会去任务队列中将已经提交的任务取出来并执行。

  • shutdownNow方法调用的是interruptWorkers()方法,该方法会逐一遍历线程池中的每一个线程(包括空闲线程和正在工作的线程)并且去中断他,所以说当调用shutdownNow方法的时候,所有线程都会被中断,等待队列中的任务不会被执行,正在执行的任务也可能会中断退出(为什么是可能而不是一定?因为如果正在执行的线程不响应中断,那么他就会继续运行)

异步模式之工作线程

定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现
就是线程池,也体现了经典设计模式中的享元模式。

例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那
么成本就太高了

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成
服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工

饥饿

固定大小线程池会有饥饿现象

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作。客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待、后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
public class TestDeadLock {
    static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = executorService.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });

//        executorService.execute(() -> {
//            log.debug("处理点餐...");
//            Future f = executorService.submit(() -> {
//                log.debug("做菜");
//                return cooking();
//            });
//            try {
//                log.debug("上菜: {}", f.get());
//            } catch (InterruptedException | ExecutionException e) {
//                e.printStackTrace();
//            }
//        });
            
    }
}

输出

17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜
17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅

当注释取消后,可能的输出

17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐... 
17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 

解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程
池,例如:

public class TestDeadLock {
    static final List<String> MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅");
    static Random RANDOM = new Random();
    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }
    public static void main(String[] args) {
        ExecutorService waiterPool = Executors.newFixedThreadPool(1);
        ExecutorService cookPool = Executors.newFixedThreadPool(1);
        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        waiterPool.execute(() -> {
            log.debug("处理点餐...");
            Future<String> f = cookPool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
    }
}

输出

17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 
17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 
17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜
17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁
创建多少线程池合适
  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存
CPU 密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因
导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程
RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

经验公式如下

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 50% = 8

例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 10% = 40

任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,
由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个
任务的延迟或异常都将会影响到之后的任务。

public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 1");
                sleep(2);
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2");
            }
        };
        // 使用 timer 添加两个任务,希望它们都在 1s 后执行
        // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
    }

输出

20:46:09.444 c.TestTimer [main] - start... 
20:46:10.447 c.TestTimer [Timer-0] - task 1 
20:46:12.448 c.TestTimer [Timer-0] - task 2 

使用 ScheduledExecutorService 改写:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
 	System.out.println("任务1,执行时间:" + new Date());
 	try { Thread.sleep(2000); } catch (InterruptedException e) { }
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {
 	System.out.println("任务2,执行时间:" + new Date());
}, 1000, TimeUnit.MILLISECONDS);

输出

任务1,执行时间:Thu Jan 03 12:45:17 CST 2019 
任务2,执行时间:Thu Jan 03 12:45:17 CST 2019 

异常的情况

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        // 添加两个任务,希望它们都在 1s 后执行
        executor.schedule(() -> {
            System.out.println("任务1,执行时间:" + new Date());
            int i = 0 / 10;
            try { Thread.sleep(2000); } catch (InterruptedException e) { }
        }, 1000, TimeUnit.MILLISECONDS);
        executor.schedule(() -> {
            System.out.println("任务2,执行时间:" + new Date());
        }, 1000, TimeUnit.MILLISECONDS);

可以看到,有异常的情况,输出并不受影响

任务2,执行时间:Mon Nov 06 16:59:26 CST 2023
任务1,执行时间:Mon Nov 06 16:59:26 CST 2023

timer中如果一个线程出现了异常,那么剩下的任务就不执行了,而ScheduledExecutorService出现了异常,那么后续任务还是会执行的

scheduleAtFixedRate 例子

以固定的速率去执行任务

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
 	log.debug("running...");
}, 1, 1, TimeUnit.SECONDS);
参数:任务对象、延时时间、执行间隔、时间单位

输出

21:45:43.167 c.TestTimer [main] - start... 
21:45:44.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:45.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:46.215 c.TestTimer [pool-1-thread-1] - running... 
21:45:47.215 c.TestTimer [pool-1-thread-1] - running... 

scheduleAtFixedRate 例子(任务执行时间超过了间隔时间):

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleAtFixedRate(() -> {
 	log.debug("running...");
 	sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s

21:44:30.311 c.TestTimer [main] - start... 
21:44:31.360 c.TestTimer [pool-1-thread-1] - running... 
21:44:33.361 c.TestTimer [pool-1-thread-1] - running... 
21:44:35.362 c.TestTimer [pool-1-thread-1] - running... 
21:44:37.362 c.TestTimer [pool-1-thread-1] - running... 

scheduleWithFixedDelay 例子:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
 	log.debug("running...");
 	sleep(2);
}, 1, 1, TimeUnit.SECONDS);

输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s

21:40:55.078 c.TestTimer [main] - start... 
21:40:56.140 c.TestTimer [pool-1-thread-1] - running... 
21:40:59.143 c.TestTimer [pool-1-thread-1] - running... 
21:41:02.145 c.TestTimer [pool-1-thread-1] - running... 
21:41:05.147 c.TestTimer [pool-1-thread-1] - running... 

评价 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线
程也不会被释放。用来执行延迟或反复执行的任务

正确处理执行任务异常

方法1:主动捉异常
ExecutorService pool = Executors.newFixedThreadPool(1);
        pool.submit(() -> {
            try {
                log.debug("task1");
                int i = 1 / 0;
            } catch (Exception e) {
                log.error("error:", e);
            }
        });

输出

21:59:04.558 c.TestTimer [pool-1-thread-1] - task1 
21:59:04.562 c.TestTimer [pool-1-thread-1] - error: 
java.lang.ArithmeticException: / by zero 
 at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) 
 at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) 
 at java.util.concurrent.FutureTask.run(FutureTask.java:266) 
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 
 at java.lang.Thread.run(Thread.java:748) 
方法2:使用 Future
ExecutorService pool = Executors.newFixedThreadPool(1);
        Future<Boolean> f = pool.submit(() -> {
            log.debug("task1");
            int i = 1 / 0;
            return true;
        });
        log.debug("result:{}", f.get());

输出

21:54:58.208 c.TestTimer [pool-1-thread-1] - task1 
Exception in thread "main" java.util.concurrent.ExecutionException: 
java.lang.ArithmeticException: / by zero 
 at java.util.concurrent.FutureTask.report(FutureTask.java:122) 
 at java.util.concurrent.FutureTask.get(FutureTask.java:192) 
 at cn.itcast.n8.TestTimer.main(TestTimer.java:31) 
Caused by: java.lang.ArithmeticException: / by zero 
 at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) 
 at java.util.concurrent.FutureTask.run(FutureTask.java:266) 
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 
 at java.lang.Thread.run(Thread.java:748) 

任务调度线程池模拟定时执行任务

如何让每周四 18:00:00 定时执行任务?

// 获得当前时间
        LocalDateTime now = LocalDateTime.now();
        // 获取本周四 18:00:00.000
        LocalDateTime thursday =
                		now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
        // 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
        if(now.compareTo(thursday) >= 0) {
            thursday = thursday.plusWeeks(1);
        }
        // 计算时间差,即延时执行时间
        long initialDelay = Duration.between(now, thursday).toMillis();
        // 计算间隔时间,即 1 周的毫秒值
        long oneWeek = 7 * 24 * 3600 * 1000;
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

        System.out.println("开始时间:" + new Date());
        executor.scheduleAtFixedRate(() -> {
            System.out.println("执行时间:" + new Date());
        }, initialDelay, oneWeek, TimeUnit.MILLISECONDS);

Tomcat线程池

Tomcat 在哪里用到了线程池呢?

剑指JUC原理-13.线程池_第4张图片

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
  • Acceptor 只负责【接收新的 socket 连接】
  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
  • Executor 线程池中的工作线程最终负责【处理请求】,也是tomcat实现高并发的保障

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

如果总线程数达到 maximumPoolSize,这时不会立刻抛 RejectedExecutionException 异常,而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常。

tomcat 7 部分源码(后续版本在该模块改动并不大)

public void execute(Runnable command, long timeout, TimeUnit unit) {
        submittedCount.incrementAndGet();
        try {
            super.execute(command);
        } catch (RejectedExecutionException rx) {
            if (super.getQueue() instanceof TaskQueue) {
                final TaskQueue queue = (TaskQueue)super.getQueue();
                try {
                    if (!queue.force(command, timeout, unit)) {
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException("Queue capacity is full.");
                    }
                } catch (InterruptedException x) {
                    submittedCount.decrementAndGet();
                    Thread.interrupted();
                    throw new RejectedExecutionException(x);
                }
            } else {
                submittedCount.decrementAndGet();
                throw rx;
            }
        }
    }

一旦超过了最大核心数了,父类的实现就会抛执行异常。

子类使用 try catch捕获到

首先拿到线程池中的阻塞队列,尝试将这个任务再次加入到队列中,如果这次成功了就不抛异常了。

TaskQueue.java

public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
        if ( parent.isShutdown() )
            throw new RejectedExecutionException(
                    "Executor not running, can't force a command into the queue"
            );
        return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task 
        is rejected
    }

Fork/Join

概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型
运算

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计
算,如归并排序、斐波那契数列、都可以用分治思想进行求解

Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运
算效率

Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

class AddTask1 extends RecursiveTask<Integer> {
    int n;
    public AddTask1(int n) {
        this.n = n;
    }
    @Override
    public String toString() {
        return "{" + n + '}';
    }
    @Override
    protected Integer compute() {
        // 如果 n 已经为 1,可以求得结果了
        if (n == 1) {
            log.debug("join() {}", n);
            return n;
        }

        // 将任务进行拆分(fork)
        AddTask1 t1 = new AddTask1(n - 1);
        t1.fork();
        log.debug("fork() {} + {}", n, t1);

        // 合并(join)结果
        int result = n + t1.join();
        log.debug("join() {} + {} = {}", n, t1, result);
        return result;
    }
}

然后提交给 ForkJoinPool 来执行

public static void main(String[] args) {
 ForkJoinPool pool = new ForkJoinPool(4);
 System.out.println(pool.invoke(new AddTask1(5)));
}

结果

[ForkJoinPool-1-worker-0] - fork() 2 + {1} 
[ForkJoinPool-1-worker-1] - fork() 5 + {4} 
[ForkJoinPool-1-worker-0] - join() 1 
[ForkJoinPool-1-worker-0] - join() 2 + {1} = 3 
[ForkJoinPool-1-worker-2] - fork() 4 + {3} 
[ForkJoinPool-1-worker-3] - fork() 3 + {2} 
[ForkJoinPool-1-worker-3] - join() 3 + {2} = 6 
[ForkJoinPool-1-worker-2] - join() 4 + {3} = 10 
[ForkJoinPool-1-worker-1] - join() 5 + {4} = 15 
15 

用图来表示

剑指JUC原理-13.线程池_第5张图片

改进

class AddTask3 extends RecursiveTask<Integer> {

    int begin;
    int end;
    public AddTask3(int begin, int end) {
        this.begin = begin;
        this.end = end;
    }
    @Override
    public String toString() {
        return "{" + begin + "," + end + '}';
    }
    @Override
    protected Integer compute() {
        // 5, 5
        if (begin == end) {
            log.debug("join() {}", begin);
            return begin;
        }
        // 4, 5
        if (end - begin == 1) {
            log.debug("join() {} + {} = {}", begin, end, end + begin);
            return end + begin;
        }

        // 1 5
        int mid = (end + begin) / 2; // 3
        AddTask3 t1 = new AddTask3(begin, mid); // 1,3
        t1.fork();
        AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
        t2.fork();
        log.debug("fork() {} + {} = ?", t1, t2);
        int result = t1.join() + t2.join();
        log.debug("join() {} + {} = {}", t1, t2, result);
        return result;
    }
}

然后提交给 ForkJoinPool 来执行

public static void main(String[] args) {
 ForkJoinPool pool = new ForkJoinPool(4);
 System.out.println(pool.invoke(new AddTask3(1, 5)));
}

结果

[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3 
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9 
[ForkJoinPool-1-worker-0] - join() 3 
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ? 
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ? 
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6 
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15 
15

用图来表示

剑指JUC原理-13.线程池_第6张图片

你可能感兴趣的:(JUC,java,开发语言,juc)