并发编程(十一)-ThreadPoolExecutor中常用方法的使用与分析

一、ThreadPoolExecutor

1. 线程池类关系图

在这里插入图片描述

  • Executor接口:是一个根接口,声明了execute(Runnable runnable)方法,执行任务代码
public interface Executor {
    void execute(Runnable command);
}
  • ExecutorService接口:是线程的主要接口,继承Executor接口,声明方法:submit、invokeAll、invokeAny、shutdown等方法
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;
}
  • AbstractExecutorService 抽象类:实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法
public abstract class AbstractExecutorService implements ExecutorService {

	protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
	
	public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }
    ......
}
  • ScheduledExecutorService接口:继承ExecutorService接口,声明定时执行任务方法
public interface ScheduledExecutorService extends ExecutorService {
  
    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
  
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
   
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);


    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

  • ThreadPoolExecutor类:是线程池的核心实现类,用来执行被提交的任务,继承AbstractExecutorService 类,实现execute、submit、shutdown、shutdownNow等方法
public class ThreadPoolExecutor extends AbstractExecutorService {
	
}
  • ScheduledThreadPoolExecutor类:可以进行延迟或者定期执行任务,继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法
public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
        
}

2. 构造方法(含7个核心参数)

  • 带7个参数的构造方法
public ThreadPoolExecutor(int corePoolSize,
		int maximumPoolSize,
		long keepAliveTime,
		TimeUnit unit,
		BlockingQueue<Runnable> workQueue,
		ThreadFactory threadFactory,
		RejectedExecutionHandler handler)
{...}
  • corePoolSize 核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  • maximumPoolSize 最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  • keepAliveTime 非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  • unit 时间单位 - keepAliveTime的时间单位
  • workQueue 用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  • threadFactory 创建线程的工厂类,可以为线程创建时起名字,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  • handler 拒绝策略,线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

3. 线程池工作方式

在这里插入图片描述

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务
  • 当线程数达到 corePoolSize 并且没有线程空闲时,这时再新加的任务会被加入 workQueue 队列排
    队,直到有空闲的线程
  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的非核心线程。
  • 如果线程到达 maximumPoolSize 后,仍然有新任务加入,这时会执行拒绝策略
  • 拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

在这里插入图片描述
其它框架的拒绝策略

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

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

二、Executors

1. newFixedThreadPool

(1) 源码描述

  • 创建一个线程池,该线程池重用固定数量的线程在共享的无界队列中运行。
  • 在任何时候,最多有 nThreads 个线程是活动的处理任务。
  • 如果在所有线程都处于活动状态时提交了额外的任务,它们将在队列中等待,直到有线程可用。
  • 如果任何线程在关闭前的执行过程中由于失败而终止,则在需要执行后续任务时,将有一个新线程代替它。
  • 线程池中的线程将一直存在,直到它被明确关闭。

源码:

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

(2) 特点

  • 核心线程数 == 最大线程数,没有非核心线程被创建,因此也无需超时时间(keepAliveTime=0)
  • 阻塞队列LinkedBlockingQueue是无界的,可以放任意数量的任务
  • 适用于任务量已知,相对耗时的任务
  • 当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常

(3) 测试

public static void main(String[] args) throws InterruptedException {
       ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            // 自定义线程工厂
            private AtomicInteger t = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "mypool_t" + t.getAndIncrement());
            }
        });

        pool.execute(() -> {
            log.debug("1");
        });

        pool.execute(() -> {
            log.debug("2");
        });

        pool.execute(() -> {
            log.debug("3");
        });
}

在这里插入图片描述

执行完任务,线程池没有关闭

2. newCachedThreadPool

(1) 描述

  • 创建一个线程池,根据需要创建新线程,但在可用时将重用先前构造的线程。
  • newCachedThreadPool线程池通常会提高执行许多短期异步任务程序的性能。
  • 如果有可用线程,调用 execute 将重用先前构造的线程。
  • 如果没有可用的现有线程,则会创建一个新线程并将其添加到线程池中。
  • 60 秒内未使用的线程将被终止并从缓存中删除。因此,保持空闲时间足够长的线程池不会消耗任何资源。
  • 请注意,可以使用 ThreadPoolExecutor 构造函数创建具有相似属性但不同细节(例如,超时参数)的线程池。

源码:

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

(2) 特点

  • 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE(全部都是临时线程),临时线程的空闲生存时间是 60s
  • 使用的是SynchronousQueue队列,没有容量,没有线程来取,任务是放不进去的
  • 注意:因为SynchronousQueue不能存放任务,所以任务过多时,会创建很多临时线程,可能会出现 OOM 或 CPU 100%
  • 因为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。所以适合任务数比较密集,但每个任务执行时间较短的情况。

(3) 测试

@Slf4j(topic = "c.TestSynchronousQueue")
public class TestSynchronousQueue {
    public static void main(String[] args) {
        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();
    }
}

21:31:51.873 c.TestSynchronousQueue [t1] - putting 1 
21:31:52.876 c.TestSynchronousQueue [t2] - taking 1
21:31:52.876 c.TestSynchronousQueue [t1] - 1 putted...
21:31:52.877 c.TestSynchronousQueue [t1] - putting...2 
21:31:53.876 c.TestSynchronousQueue [t3] - taking 2
21:31:53.877 c.TestSynchronousQueue [t1] - 2 putted...
t1生成任务后,t2立马知道有任务,没有任务就阻塞住,因为队列没有容量,只是充当桥梁

3. newSingleThreadExecutor

(1) 描述

  • 创建一个 Executor,它使用单个工作线程在无界队列中运行。
    (但是请注意,如果这个单线程在关闭之前的执行过程中由于失败而终止,如果需要执行后续任务,一个新线程将取而代之。)

自己创建一个线程 和 线程池创建一个固定线程 的区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,剩余的任务将不会执行
  • 而线程池还会新建一个线程,保证线程池的正常工作

newSingleThreadExecutor 与 newFixedThreadPool 区别:

  • Executors.newSingleThreadExecutor()线程个数始终为1,不能修改,可以保证任务按顺序执行。
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改。
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改。

源码:

public static ExecutorService newSingleThreadExecutor() {
    // FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,
    // ThreadPoolExecutor被包装在FinalizableDelegatedExecutorService 中,因此不能调用 ThreadPoolExecutor 中特有的方法
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

(2) 特点

  • 线程数固定为 1,任务数多于 1 时,会放入LinkedBlockingQueue无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
  • 适合多个任务排队执行。
  • 注意:LinkedBlockingQueue不指定容量,可能会造成OOM内存溢出。

(3) 测试

public static void main(String[] args) throws InterruptedException {
   ExecutorService pool = Executors.newSingleThreadExecutor();
        pool.execute(() -> {
            log.debug("1");
            int i = 1 / 0;
        });

        pool.execute(() -> {
            log.debug("2");
        });

        pool.execute(() -> {
            log.debug("3");
        });
}
22:08:40.799 c.TestExecutors [pool-1-thread-1] - 1
22:08:40.803 c.TestExecutors [pool-1-thread-2] - 2
22:08:40.804 c.TestExecutors [pool-1-thread-2] - 3
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
	at cn.itcast.n8.TestExecutors.lambda$test2$0(TestExecutors.java:19)
	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)
虽然线程1出现异常结束了,但是线程池创建了一个新线程将剩余任务执行完了

4. newScheduledThreadPool

(1) 描述

  • 创建一个固定长度线程池,支持延迟及周期性任务执行,属于任务调度线程池

源码:

ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor 类,实现了 ScheduledExecutorService接口。
底层实际还是通过ThreadPoolExecutor创建线程池;
可以实现ScheduledExecutorService中schedule、scheduleAtFixedRate、scheduleWithFixedDelay方法,进行延迟及周期性任务执行;
核心线程数是手动设置的,
最大线程数是无限的(Integer.MAX_VALUE相当于无限),
生存时间0表示不释放临时线程,
使用的是DelayedWorkQueue延迟队列;

// 只用传入核心线程数
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public interface ScheduledExecutorService extends ExecutorService {

    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}

(2) 特点

  • 线程数固定,任务数多于线程数时,会放入DelayedWorkQueue延迟队列排队
  • 任务执行完毕,这些线程也不会被释放,用来执行延迟或周期性的任务

(3) newScheduledThreadPool 与 Timer 对比

Timer:

  • 在『任务调度线程池』功能加入之前,可以使用 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");
            }
        };

        log.debug("start...");
        // 使用 timer 添加两个任务,希望它们都在 1s 后执行
 		// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
}

在这里插入图片描述

newScheduledThreadPool 延时执行任务:

  • 存在多个线程时,同一时间可以有多个任务‘’
  • 前一个任务的延迟或异常不会影响到之后的任务
public static void main(String[] args) throws ExecutionException, InterruptedException {
	ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
		pool.schedule(() -> {
            log.debug("task1");
            // 出现异常也不会影响其他线程
            int i = 1 / 0;
        }, 1, TimeUnit.SECONDS);

        pool.schedule(() -> {
            log.debug("task2");
        }, 1, TimeUnit.SECONDS);
}

在这里插入图片描述

5. scheduleAtFixedRate

(1) 描述

  • 创建并执行一个周期性动作,在给定的初始延迟后首先启用,然后在给定的时间段内启用,即执行将在 initialDelay 之后开始,然后是 initialDelay+period,然后是 initialDelay + 2 period,依此类推

源码:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

(2) 特点

  • 如果任务的任何执行遇到异常,则后续执行将被抑制。否则,任务只会通过取消或终止执行程序而终止
  • 如果此任务的任何执行时间超过其周期,则后续执行可能会延迟开始,但不会并发执行 (即执行时间超过间隔时间时,间隔时间会延长)
    例如:间隔时间设置为1s,但执行时间花了2S,所以间隔时间会多等1s再执行下一个任务,后续就是每隔2s执行一次

(3) 测试

public static void main(String[] args) {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    log.info("start...");
    pool.scheduleAtFixedRate(() -> {
        log.info("running...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, 1, 1, TimeUnit.SECONDS);
}
23:39:41.884 c.TestTimer [main] - start...
23:39:42.948 c.TestTimer [pool-1-thread-1] - running...
23:39:44.949 c.TestTimer [pool-1-thread-1] - running...
23:39:46.949 c.TestTimer [pool-1-thread-1] - running...
23:39:48.949 c.TestTimer [pool-1-thread-1] - running...
。。。。
输出结果分析:一开始,延时 1s,接下来,由于任务执行时间(2s) 大于 间隔时间(1S),间隔被『撑』到了 2s

(4) 定时任务案例

  • 让每周三 10:00:00 定时执行任务
public static void main(String[] args) {
        //  获取当前时间
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);
        // 获取周三时间
        LocalDateTime time = now.withHour(10).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.WEDNESDAY);
        // 如果 当前时间 > 本周周三,必须找到下周周三
        if(now.compareTo(time) > 0) {
            time = time.plusWeeks(1);
        }
        System.out.println(time);
        // initailDelay 代表当前时间和周三的时间差
        // period 一周的间隔时间
        long initailDelay = Duration.between(now, time).toMillis();
        long period = 1000 * 60 * 60 * 24 * 7;
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        pool.scheduleAtFixedRate(() -> {
            System.out.println("running...");
        }, initailDelay, period, TimeUnit.MILLISECONDS);
    }
2022-06-16T10:00:20.100 // 上周四,超过了周三
2022-06-22T10:00	// 所以从下周三开始
running...
running...
running...
running...
。。。。

6. scheduleWithFixedDelay

(1) 描述

  • 创建并执行一个周期性动作,该动作首先在给定的初始延迟后启用,等待 上一个任务执行花费时间 + 间隔时间,之后再执行下一个任务

源码:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

(2) 特点

  • 如果任务的任何执行遇到异常,则后续执行将被抑制。否则,任务只会通过取消或终止执行程序而终止
  • 当上一个任务执行完后,下一个任务会再等一个间隔时间执行
    例如:间隔时间设置为1s,但执行时间花了2S,所以下一个任务会在第3s后执行,后续就是每隔3s执行一次

(3) 测试

public static void main(String[] args) {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
    log.info("start...");
    pool.scheduleWithFixedDelay(() -> {
        log.info("running...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, 1, 1, TimeUnit.SECONDS);
}
23:47:45.050 c.TestTimer [main] - start...
23:47:46.138 c.TestTimer [pool-1-thread-1] - running...
23:47:49.140 c.TestTimer [pool-1-thread-1] - running...
23:47:52.141 c.TestTimer [pool-1-thread-1] - running...
23:47:55.142 c.TestTimer [pool-1-thread-1] - running...
。。。。
输出结果分析:一开始,延时 1s,上一个任务执行完花了2S,所以间隔都是 3s

三、提交任务的方法

1. execute

// 执行任务
void execute(Runnable command);
public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        pool.execute(() -> {
            System.out.println(Thread.currentThread().getName() + ":执行任务");
        });
}

在这里插入图片描述

2. submit

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
public static void main(String[] args) throws ExecutionException, InterruptedException {
       ExecutorService pool = Executors.newFixedThreadPool(1);
       Future<String> future = pool.submit(() -> {
           log.debug("running");
           Thread.sleep(1000);
           return "ok";
       });

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

在这里插入图片描述

3. invokeAll

// 提交 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;
public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        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();
            }
        });
 }

在这里插入图片描述

同时提交3个任务后,有2个任务先被2个线程执行,最快的任务500ms执行完,再执行第三个任务,
最后所有任务执行完花了2s(取执行任务最长时间),同时打印结果

4. invokeAny

// 提交 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(2);
       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);
}

在这里插入图片描述
同时提交3个任务,有2个任务先被2个线程执行,最快的任务500ms执行完,返回此任务执行结果,其它任务不再执行

四、关闭线程池的方法

1. shutdown

  • 线程池状态变为 SHUTDOWN
  • 不会接收新任务
  • 但已提交任务会执行完
  • 此方法不会阻塞调用线程的执行(例如:main线程调了shutdown方法后,main线程不会被阻塞)

源码:

public void shutdown() {
     final ReentrantLock mainLock = this.mainLock;
      mainLock.lock();
      try {
          checkShutdownAccess();
          // 修改线程池状态
          advanceRunState(SHUTDOWN);
          // 仅会打断空闲线程
          interruptIdleWorkers();
          onShutdown(); // 扩展点 hook for ScheduledThreadPoolExecutor
      } finally {
          mainLock.unlock();
      }
      // 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)
      tryTerminate();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> result1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finish...");
            return 1;
        });

        Future<Integer> result2 = pool.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finish...");
            return 2;
        });

        Future<Integer> result3 = pool.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finish...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();
        log.debug("main 线程继续执行");

		pool.submit(() -> {
            log.debug("task 4 running...");
            Thread.sleep(1000);
            log.debug("task 4 finish...");
            return 4;
        });
}
23:12:15.692 c.TestShutDown [main] - shutdown
23:12:15.692 c.TestShutDown [pool-1-thread-2] - task 2 running...
23:12:15.692 c.TestShutDown [pool-1-thread-1] - task 1 running...
23:12:15.696 c.TestShutDown [main] - main 线程继续执行
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@11531931 rejected from java.util.concurrent.ThreadPoolExecutor@5e025e70[Shutting down, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at cn.itcast.n8.TestShutDown.main(TestShutDown.java:43)
23:12:16.697 c.TestShutDown [pool-1-thread-1] - task 1 finish...
23:12:16.697 c.TestShutDown [pool-1-thread-1] - task 3 running...
23:12:16.698 c.TestShutDown [pool-1-thread-2] - task 2 finish...
23:12:17.699 c.TestShutDown [pool-1-thread-1] - task 3 finish...

  • 调用shutdown后,正在执行的线程、已经提交到任务队列的线程都不会终止,直到执行结束,线程池才关闭
  • 调用shutdown后,不会阻塞调用线程的执行
  • 调用shutdown后,不能再添加任务,否则报错

2. shutdownNow

  • 线程池状态变为 STOP
  • 不会接收新任务
  • 会将队列中的任务返回
  • 并用 interrupt 的方式中断正在执行的任务
 public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            // 修改线程池状态
            advanceRunState(STOP);
            // 打断所有线程
            interruptWorkers();
            // 获取队列中剩余任务
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        // 尝试终结(前面已经打断了所有线程,所以这一步没有运行的线程了,会立刻停止)
        tryTerminate();
        return tasks;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> result1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finish...");
            return 1;
        });

        Future<Integer> result2 = pool.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finish...");
            return 2;
        });

        Future<Integer> result3 = pool.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finish...");
            return 3;
        });

        log.debug("shutdownNow");
        List<Runnable> runnables = pool.shutdownNow();
        log.debug("返回队列中还没执行的任务:{}", runnables);

    }
23:25:54.085 c.TestShutDown [main] - shutdownNow
23:25:54.085 c.TestShutDown [pool-1-thread-2] - task 2 running...
23:25:54.085 c.TestShutDown [pool-1-thread-1] - task 1 running...
23:25:54.088 c.TestShutDown [main] - 返回队列中还没执行的任务:[java.util.concurrent.FutureTask@47f37ef1]

3. 其他方法

// 不在 RUNNING 状态的线程池,此方法就返回 true
boolean isShutdown();
// 线程池状态是否是 TERMINATED 终止状态
boolean isTerminated();
// 调用 shutdown 后,由于调用线程(例如:mian线程)并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待(main线程调用shutdown后, 再调用awaitTermination,main线程就会等待)
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        Future<Integer> result1 = pool.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finish...");
            return 1;
        });

        Future<Integer> result2 = pool.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finish...");
            return 2;
        });

        Future<Integer> result3 = pool.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finish...");
            return 3;
        });

        log.debug("shutdown");
        pool.shutdown();
        log.debug("main 线程等待所有任务终止后再执行");
        pool.awaitTermination(3, TimeUnit.SECONDS); // 最多等3s
        log.debug("main 线程等完后继续执行");
    }
23:15:25.578 c.TestShutDown [main] - shutdown
23:15:25.581 c.TestShutDown [main] - main 线程等待所有任务终止后再执行
23:15:25.578 c.TestShutDown [pool-1-thread-2] - task 2 running...
23:15:25.578 c.TestShutDown [pool-1-thread-1] - task 1 running...
23:15:26.582 c.TestShutDown [pool-1-thread-1] - task 1 finish...
23:15:26.582 c.TestShutDown [pool-1-thread-2] - task 2 finish...
23:15:26.582 c.TestShutDown [pool-1-thread-2] - task 3 running...
23:15:27.582 c.TestShutDown [pool-1-thread-2] - task 3 finish...
23:15:27.582 c.TestShutDown [main] - main 线程等完后继续执行

所有任务执行完没有超过3s,所以awaitTermination可以提前等待结束

五、 多线程设计模式之工作线程模式

1. 定义

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

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

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

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

2. 饥饿现象

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

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,线程数不够出现饥饿现象

案例:

  • (1)1个线程池,2个核心线程,做2个任务
@Slf4j(topic = "c.TestDeadLock")
public class TestStarvation {

    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 pool = Executors.newFixedThreadPool(2);
	
		// pool
        pool.execute(() -> {
            log.debug("处理点餐...");
            // pool
            Future<String> f = pool.submit(() -> {
                log.debug("做菜");
                return cooking();
            });
            try {
            	// 等待前一个做菜任务
                log.debug("上菜: {}", f.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });

    }
}

在这里插入图片描述

1个线程池,2个核心线程,做2个任务,可以成功做完
  • (2)1个线程池,2个核心线程,做4个任务
Slf4j(topic = "c.TestDeadLock")
public class TestStarvation {

    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 pool = Executors.newFixedThreadPool(2);

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

    }
}

在这里插入图片描述

1个线程池,2个核心线程,做4个任务,出现了饥饿现象,由于“上菜"需要等到"做菜"完成,
所以2个线程都在等待时,没有足够的线程去“做菜”,导致不能成功执行完

解决饥饿:

  • 方法1:可以增加线程池的大小,不过不是根本解决方案
  • 方法2:根据不同的任务类型,采用不同的线程池
@Slf4j(topic = "c.TestDeadLock")
public class TestStarvation {

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

    }
}

在这里插入图片描述

六、配置线程池大小

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

1. CPU 密集型运算

  • 对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是:
    • 通常采用 线程数 = CPU核数+1(也可以设置成 CPU核数*2) 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

2. I/O 密集型运算

  • CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行
  • 对于IO密集型的应用,可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率

经验公式如下:

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

例如: 4CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8
例如: 4CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40

公式二:线程数 = CPU核心数/(1-阻塞系数) 这个阻塞系数一般为0.8~0.9之间,也可以取0.8或者0.9。

套用公式,对于双核CPU来说,它比较理想的线程数就是20,当然这都不是绝对的,需要根据实际情况以及实际业务来调整:
final int poolSize = (int)(cpuCore/(1-0.9))
针对于阻塞系数,《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》中有提到一句话:
对于阻塞系数,我们可以先试着猜测,抑或采用一些细嫩分析工具或java.lang.management API 
来确定线程花在系统/IO操作上的时间与CPU密集任务所耗的时间比值。

七、处理线程执行任务时的异常

1. 主动捕获异常

  • try-catch 主动捕获异常
public static void main(String[] args) {
   ExecutorService pool = Executors.newFixedThreadPool(1);
        pool.submit(() -> {
            try {
                log.debug("task1");
                int i = 1 / 0;
            } catch (Exception e) {
                log.error("error:", e);
            }
        });
}
23:59:10.093 c.TestTimer [pool-1-thread-1] - task1
23:59:10.100 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 返回异常

  • 提交Callable 任务,配合 Future 可以获取返回值
    • 没有异常时,返回的是正常值
    • 有异常时,返回的是异常信息
public static void main(String[] args) throws ExecutionException, InterruptedException {
	ExecutorService pool = Executors.newFixedThreadPool(1);
        Future<Boolean> f = pool.submit(() -> {
            log.debug("task1");
            int i = 1 / 0;
            return true;
        });
        log.debug("result:{}", f.get());
}
00:00:50.799 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:40)
Caused by: java.lang.ArithmeticException: / by zero
	at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:37)
	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)
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值