体系化深入学习并发编程(三)更好地了解Java线程池

线程池

  • 关于线程池
  • 创建和停止线程池
    • 线程池构造函数的参数
    • 线程池创建应该手动还是自动
    • 线程池中的线程数量
    • 线程池的停止
  • 拒绝任务
    • 何时拒绝
    • 拒绝策略
  • 钩子方法
  • 线程池的底层原理
    • 线程池的组成
    • Executor家族
    • 线程复用的原理

关于线程池

线程是一个比较昂贵的资源。
比如线程的创建和启动、线程的销毁、线程调度的开销等
因此,我们需要一种有效使用线程的方式。这就是线程池
类似线程池这种的对象池(比如数据库连接池),实现方式就是需要的时候,就去池中获取一个对象,用完后还回池中。
线程池节省了不断创建和销毁线程的开销,可以控制线程的数量合理利用CPU资源和内存,并且能够统一管理。

创建和停止线程池

线程池构造函数的参数

  1. corePoolSize
    核心线程数:默认情况下,线程池初始化后,池内没有任何线程,等待任务到来,再创建新线程执行任务

  2. maxPoolSize
    最大线程池容量:线程池中线程的最大容量。

  3. keepAliveTime
    线程存活时间:当线程池中线程多于corePoolSize时,如果多出的线程的空闲时间超过存活时间,就会回收线程

  4. ThreadFactory
    线程工厂:通过工厂模式创建线程,默认使用Executors.defaultThreadFactory(),创建的线程都是非守护线程,都在同一线程组,且都是5的优先级。

  5. workQueue
    工作队列:
    1.直接交接队列(synchronousQueue):不存储任务,直接中转任务给线程
    2.无界队列(LinkedBlockingQueue):无限存储任务,导致maxPoolSize无意义,且不会增加线程数,可能导致OOM
    3.有界队列(ArrayBlockingQueue):有限容量,队列满了会判断是否创建新线程。

  6. handler
    拒绝任务处理器:当池中最大线程容量和工作队列容量使用有限边界并且饱和时,会拒绝新的任务。

如果一个任务来了,线程池中线程数量少于corePoolSize,则创建新的线程在池中;如果池中线程数量大于或等于corePoolSize且小于maxPoolSize,就将任务放入队列;如果队列满了,线程数量少于maxPoolSize,就创建新的线程;如果线程池数量达到maxPoolSize,且队列也满了,就拒绝任务
体系化深入学习并发编程(三)更好地了解Java线程池_第1张图片

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

线程池应该手动创建,这样可以自己设置线程池的运行规则,避免资源耗尽的风险

  1. 使用newFixedThreadPool创建新的固定线程池
public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i <100; i++) {
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }

打印结果:只有三个线程在执行任务.
原因:见源码

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

只需要传入线程数量,核心线程数和最大线程数相同,并且工作队列是无界的。
当工作队列加入速度大于线程执行速度,会造成大量内存被占用,最终可能导致OOM的发生。

  1. 使用newSingleThreadExecutorl创建单个线程的线程池
 public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i <100; i++) {
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }

该方法源码如下:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  1. 使用newCachedThreadPool创建缓存线程池
 public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i <100; i++) {
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }

源码如下:

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

该方法使用的是直接交接队列,并不存储任务,而最大容量设为整型最大值。每来一个任务就创建一个线程,如果60后有空闲线程,就回收空闲线程。
但是如果线程过多有可能导致OOM

  1. 使用newScheduledThreadPool创建缓存线程池
//传入核心线程数
ScheduledExecutorService scheduledExecutorService
 = Executors.newScheduledThreadPool(5);
 //该方法表示多少时间后执行任务
 //传入三个参数,分别传入执行任务,计时时间,时间单位
scheduledExecutorService.schedule(()->{
            System.out.println(Thread.currentThread().getName());
        },5, TimeUnit.SECONDS);
//该方法表示多少时间后重复执行
//传入四个参数,分别传入执行任务,开始时间,计时时间,时间单位
scheduledExecutorService.scheduleAtFixedRate(()->{
            System.out.println(Thread.currentThread().getName());
        },1,5, TimeUnit.SECONDS);

源码:

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

该线程池主要是用于时间相关的。
这四种常见线程池也是各有千秋,都是已经设计好了的,但并不一定是我们想要的,所以为了满足自身需要,我们应该自己手动创建线程池。

线程池中的线程数量

根据上文我们得出结论:应该手动创建线程池
那么手动创建线程池则需要考虑到线程池中的线程数量的多少。
如果线程数量少了,会造成CPU的空闲导致CPU资源浪费。而如果线程过多,频繁的上下文切换也会导致性能的开销,反而可能降低程序的效率。所以设置一个线程的数量是很有讲究的。

  • 对于CPU密集型线程,这类线程主要消耗的是CPU资源,通常线程数量设置为CPU的1-2倍之间
  • 对于I/O密集型线程,这类线程可能会在进行等待,如果少了可能也会造成CPU资源的浪费,线程数应设置为:
    CPU核心数 X(1+平均等待时间/平均执行时间)

线程池的停止

  1. shutdown()
    停止该线程池,执行该方法后,通知线程池该停止了。然后线程池会拒绝新的任务,将线程池内所有任务执行完毕后,停止线程池。如果在此期间再次传入任务,会被拒绝同时抛出异常。

  2. isShutdown()
    返回boolean类型,判断当前线程池是否接收到停止命令。

  3. isTerminated
    返回boolean类型,判断当前线程池所有任务是否运行完成。

  4. awaitTermination
    返回boolean类型,传入两个参数,分别是计时时间和时间单位。这个方法的作用是:判断在传入的时间内,线程是否运行完所有任务。
    方法执行后会进入阻塞状态。如果线程池任务执行完了,就立即返回true;如果超时还没执行完,就返回false;如果被打断了会抛出异常。

  5. shutdownNow
    该方法比较暴力,向线程池内所有线程发送打断信号,所有的线程会对打断信号进行处理,而队列中剩余的任务会被作为List集合的对象返回。

五种方法的演示:

public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i <1000; i++) {
            executorService.execute(()->{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()
                            +"被中断了");
                }
                System.out.println(Thread.currentThread().getName()+
                        " "+executorService.isShutdown()+
                        " "+executorService.isTerminated());
            });
        }
        TimeUnit.SECONDS.sleep(1);
        executorService.shutdown();
        TimeUnit.SECONDS.sleep(1);
        List<Runnable> runnableList = executorService.shutdownNow();
        System.out.println("是否运行完?"
                +executorService.awaitTermination(10, TimeUnit.SECONDS));
        System.out.println("剩余任务数量:"+runnableList.size());
        TimeUnit.SECONDS.sleep(1);
        System.out.println("是否终止?"+executorService.isTerminated());

    }

拒绝任务

根据之前谈到的,某些线程池有时是会拒绝新的任务的。

何时拒绝

那么线程池何时会拒绝任务呢?

  1. 线程池中所有线程都在运行,并且队列中的任务数量也饱和了的时候,此时就会拒绝新的任务
  2. 线程池已经终止了,此时也会拒绝新到来的任务。

拒绝策略

  1. AbortPolicy
    中止策略:直接抛出拒绝任务的异常。

  2. DiscardPolicy
    丢弃策略:不作任何通知,直接丢弃新的任务

  3. DiscardOldestPolicy
    丢弃最老策略:这种策略会丢弃队列中最老的任务,为新任务腾出空间

  4. CallerRunsPolicy
    调用者执行策略:线程A传进来这个任务,就让线程A来运行。好处就是保证了业务不会丢失,同时负反馈任务传递速率。

钩子方法

钩子方法可以在任务执行之前和之后调用。可以用来操纵执行环境,记录日志,作统计等。

  1. beforeExecute(Thread t,Runnable r)
    在给定的线程中执行给定的Runnable之前调用方法

  2. afterExecute(Thread t,Runnable r)
    完成指定Runnable的执行后调用方法

public class HookMethodsThreadPool extends ThreadPoolExecutor {
    public HookMethodsThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        System.out.println("开始保存任务日志");
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        System.out.println("任务日志保存完毕");
    }

    public static void main(String[] args) {
        HookMethodsThreadPool hookMethodsThreadPool = new HookMethodsThreadPool(3, 5, 0,
                TimeUnit.SECONDS, new LinkedBlockingQueue());
        for (int i = 0; i <10; i++) {
            hookMethodsThreadPool.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

打印结果如下:

...
开始保存任务日志
pool-1-thread-2
任务日志保存完毕
开始保存任务日志
pool-1-thread-1
任务日志保存完毕
...

线程池的底层原理

线程池的组成

  1. 线程池管理器
    主要是管理线程池,比如创建线程池,停止线程池
  2. 工作线程
    被创建出来执行任务的线程
  3. 任务队列
    存放任务的队列,为了支持并发,所以使用线程安全的BlockingQueue
  4. 任务接口
    队列中存储的任务

Executor家族

  1. Executor
    最顶层的接口,只有一个execute(Runnable command)方法
  2. ExecutorService extends Executor
    继承了Executor的接口,添加了一些管理线程池的方法,比如之前提到的终止线程池的五个方法
  3. AbstractExecutorService implements ExecutorService
    一个抽象类,实现了ExecutorService接口,实现了关于Future的一些方法的逻辑
  4. ThreadPoolExecutor extends AbstractExecutorService
    平时理解的线程池
  5. Executors
    和上面并没有继承关系,该类继承Object类,是一个工具类,提供了许多关于线程池的方法。

线程复用的原理

线程如何做到复用的呢?直接查看核心的源码
每次从工作队列中取出新的任务

Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;

然后使用while循环,线程不会停止,会不断地去任务队列中获取新的任务

while (task != null || (task = getTask()) != null)

调用task的run方法,而在执行前后有之前提到的钩子方法

try {
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
            task.run();
        } catch(...) {
            ....
        } finally {
        	afterExecute(task, thrown);
        }
}

你可能感兴趣的:(Java)