Java并发线程池原理解析

Posted 愉悦滴帮主)

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发线程池原理解析相关的知识,希望对你有一定的参考价值。

线程

1. 概述

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

2. 生命周期:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。尤其是当线程启动以后,它不能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。


线程池

1. 概述

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。

任务调度以执行线程的常见方法是使用同步队列,称作任务队列。池中的线程等待队列中的任务,并把执行完的任务放入完成队列中。

线程池模式一般分为两种:HS/HA半同步/半异步模式、L/F领导者与跟随者模式。

  • 半同步/半异步模式又称为生产者消费者模式,是比较常见的实现方式,比较简单。分为同步层、队列层、异步层三层。同步层的主线程处理工作任务并存入工作队列,工作线程从工作队列取出任务进行处理,如果工作队列为空,则取不到任务的工作线程进入挂起状态。由于线程间有数据通信,因此不适于大数据量交换的场合。

  • 领导者跟随者模式,在线程池中的线程可处在3种状态之一:领导者leader、追随者follower或工作者processor。任何时刻线程池只有一个领导者线程。事件到达时,领导者线程负责消息分离,并从处于追随者线程中选出一个来当继任领导者,然后将自身设置为工作者状态去处置该事件。处理完毕后工作者线程将自身的状态置为追随者。这一模式实现复杂,但避免了线程间交换任务数据,提高了CPU cache相似性。在ACE(Adaptive Communication Environment)中,提供了领导者跟随者模式实现。

2. 工作原理

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

3.案例分析

package com.tguo.demo;

public class ThreadDemo extends Thread
    private String name;

    public ThreadDemo(String name) 
        this.name = name;
    
    
    @Override
    public void run() 
        System.out.println(name);
    

    public static void main(String[] args) 
        new ThreadDemo("你好线程run").run();
        new ThreadDemo("你好线程start").start();
    


打印:
你好线程run
你好线程start

上述代码中:.run是方法调用,说明只启动了mian方法的主线程。.start是线程调用,也就是说主线程启动的时候,Thread线程也会启动,Thread线程调用run方法。


线程池的使用

1.案例

/**
 * 线程池执行
 */
@SuppressWarnings("all")
public class ThreadPoolTest 
    public static void main(String[] args) throws InterruptedException 
        Long start  = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<>();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100000; i++) 
            executorService.execute(new Runnable() 
                @Override
                public void run() 
                    list.add(random.nextInt());
                
            );
        
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.DAYS);
        System.out.println("时间"+(System.currentTimeMillis()-start));
        System.out.println("大小"+list.size());
    


打印:
时间94
大小100000

我们不用线城池的方式:同样执行上述逻辑得到的结果是时间(29815,大小100000)。那么为什么线程池的效率这么高?下面我们来分析Java中的几种远程池的工作原理。

2.Executor 框架结构(主要由三大部分组成)

1) 任务(Runnable /Callable)

执行任务需要实现的 Runnable 接口 或 Callable接口。Runnable 接口或 Callable 接口 实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。

2) 任务的执行(Executor)

如下图所示,包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口。

3) 异步计算的结果(Future)

Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。

当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象。


这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

注意: 通过查看 ScheduledThreadPoolExecutor 源代码我们发现 ScheduledThreadPoolExecutor 实际上是继承了 ThreadPoolExecutor 并实现了 ScheduledExecutorService ,而 ScheduledExecutorService 又实现了 ExecutorService,正如我们下面给出的类关系图显示的一样。

3.线程池四种实现方式

CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
 

4.线程池效率对比

package com.tguo.demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SuppressWarnings("all")
public class ThreadPoolDemo 
    public static void main(String[] args) 
        ExecutorService executorService1 = Executors.newFixedThreadPool(10); //慢
//        ExecutorService executorService2 = Executors.newCachedThreadPool(); //快
//        ExecutorService executorService3 = Executors.newSingleThreadExecutor(); //最慢
        for (int i = 0; i < 100; i++) 
            executorService1.execute(new MyTask(i));
        
    

class MyTask implements  Runnable
   int i = 0;

    public MyTask(int i) 
        this.i = i;
    

    @Override
    public void run() 
        System.out.println(Thread.currentThread().getName()+"---"+i);
        try 
            Thread.sleep(1000L);
         catch (InterruptedException e) 
            e.printStackTrace();
        

    

 我们通过对比FixedThreadPool,CachedThreadPool,SingleThreadPool三种线程池的方式。发现CachedThreadPool最快,FixedThreadPool中等,SingleThreadPool最慢。


线城池原理分析

构造函数的参数含义


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

corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;核心线程数

maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;非核心线程数

keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;存活时间

unit:keepAliveTime的单位,时间单位

workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;

threadFactory:线程工厂,用于创建线程,一般用默认即可;

handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;


1. FixedThreadPool 

构造方法:表示核心线程数,线程存活时间为0,任务队列。我们可以手动传入核心线程数。

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

工作流程

假设:核心线程数为10,有100个任务要处理。首先10个线程处理十个任务。剩余90个处理不了就放入任务队列中。然后再处理任务队列中的10个任务,此时任务队列有80个任务。依次类推。每次同时处理十个任务。 

不足:FixedThreadPool 的队列是无界队列,如果要处理的任务非常多的话,会导致内存溢出。

2. CachedThreadPool

构造方法:表示核心线程数为0,非核心线程数无限大(有多少个任务就有多少个非核心线程),线程存活时间为0,任务队列(注意这里的任务队列只有一个格子,也就是说只能存储一个任务)。

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

工作流程: 

假设:如果有一个任务,则由将这个任务放入队列中,然后创建一个Worker去处理这个任务。

         如果由一百个任务,则将第一个任务放入队列中, 然后创建一个Worker去处理这个任务,这时发现剩余99个任务队列中放不下,则创建99个Worker去处理这些任务。也就是说有多少个任务就创建多少个线程。

线程复用:比如线程1正在处理任务1,然后又来一个任务则创建线程2处理任务2,这时又来一个任务3,但是线程1已经处理完毕了,则就可以交给线程1继续处理任务3。

不足:CachedThreadPool的非核心线程数是无限的,也会导致CPU百分之百的问题。

3.SingleThreadExecutor

构造方法:表示核心线程数只有一个,非核心线程数为1,线程存活时间为0,任务队列。

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

工作流程:  

表示:只有一个线程在处理任务。其他剩余任务存入队列中。

不足:SingleThreadExecutor这个线程池与单线程没什么区别。效率慢。


自定义线城池

案例:

@SuppressWarnings("all")
public class ThreadPoolDemo 
    public static void main(String[] args) 
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
        for (int i = 0; i < 100; i++) 
            threadPoolExecutor.execute(new MyTask(i));
        
    


class MyTask implements Runnable 
    int i = 0;

    public MyTask(int i) 
        this.i = i;
    

    @Override
    public void run() 
        System.out.println(Thread.currentThread().getName() + "---" + i);
        try 
            Thread.sleep(1000L);
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

打印结果:通过下面的打印结果,我们发现报错了。这时因为线程池的拒绝策略。我们下面分析线程池的拒绝策略。 

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.tguo.demo.MyTask@1d44bcfa rejected from java.util.concurrent.ThreadPoolExecutor@266474c2[Running, pool size = 20, active threads = 20, queued tasks = 10, 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 com.tguo.demo.ThreadPoolDemo.main(ThreadPoolDemo.java:16)
pool-1-thread-2---1
pool-1-thread-1---0
pool-1-thread-3---2
pool-1-thread-4---3
pool-1-thread-5---4
pool-1-thread-6---5
pool-1-thread-7---6
pool-1-thread-8---7
pool-1-thread-9---8
pool-1-thread-11---20

 原因:假设我们有40个任务要处理,首先前十个任务(1-10)交给核心线程数处理,然后任务队列中存储十个任务(11-20),最后由非核心线程处理(因为非核心线程数要减去核心线程数),比如定义非核心线程数为20,核心线程数为10,那么非核心线程数实际为(非核心线程数-核心线程数)所以这里非核心线程数只能处理十个(21-30)任务。这时第31个任务进来,不能进行处理则抛出异常。


线程池的拒绝策略

结构图

 拒绝策略参考:https://zhuanlan.zhihu.com/p/69790401

(1)AbortPolicy

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

A handler for rejected tasks that throws a @code RejectedExecutionException.

这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

(2)DiscardPolicy

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。

A handler for rejected tasks that silently discards therejected task.

使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。

(3)DiscardOldestPolicy

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

A handler for rejected tasks that discards the oldest unhandled request and then retries @code execute, unless the executor is shut down, in which case the task is discarded.

此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。

(4)CallerRunsPolicy

ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

A handler for rejected tasks that runs the rejected task directly in the calling thread of the @code execute method, unless the executor has been shut down, in which case the task is discarded.

 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务

线城池处理优先级 

提交优先级: 核心线程 > 工作队列 > 非核心线程


执行优先级: 核心线程 > 非核心线程 > 工作队列

以上是关于Java并发线程池原理解析的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程(十三)-- 线程池实现原理

java线程池原理解析

java线程池原理解析

深入浅出JAVA线程池使用原理1

Java中的线程池原理分析及使用

Java并发编程--ThreadPoolExecutor