vlambda博客
学习文章列表

Java 线程和线程池

一、线程状态



线程共有五种状态:New 、Runnable 、Running 、Blocked 、Dead,如下图所示:


1.New (新创建)


当用new操作符创建一个线程时,如new Thread®,该线程还没有开始运行。这时它的状态是new。此时程序还没有开始运行线程中的代码,在线程运行之前还有一些基础工作要做。


2.Runnable (可运行/就绪)


一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。


处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。


3.Running (运行中)


当线程获得CPU时间片,线程就进入Running状态。

处于Running状态的线程有可能在运行中CPU时间片用完,而run方法没运行完,线程就又进入Runnable状态。

通常情况下,运行中的线程一直处于Running与Runnable交替转换的过程中。

  

4.Blocked (等待/阻塞/睡眠)


当线程在Running状态中,遇到阻塞等待锁、等待用户输入、调用sleep()方法、调用join等待其他线程情况,会导致线程进入阻塞状态(Blocked)。

处于阻塞状态的线程,在阻塞等待结束之后,会进入Runnable状态,等获得CPU时间片继续运行程序。


线程运行过程中,可能由于各种原因进入阻塞状态:

  • 线程通过调用sleep方法进入睡眠状态;

  • 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;

  • 线程试图得到一个锁,而该锁正被其他线程持有;

  • 线程在等待某个触发条件;


5.Dead (死亡)


有两个原因会导致线程死亡:

  •    run方法正常退出而自然死亡;

  • 一个未捕获的异常终止了run方法而使线程猝死;




二、线程池构造函数



public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue<Runnable> workQueue,                          ThreadFactory threadFactory,                          RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }


接下来我们分别讲解这些参数的含义:

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


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


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


  • unit:keepAliveTime的单位


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

Java 线程和线程池


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


  • handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务,线程池给我们提供了四种常见的拒绝策略,如下图所示:

Java 线程和线程池

这四种策略各有优劣,比较常用的是DiscardPolicy,但是这种策略有一个弊端就是任务执行的轨迹不会被记录下来。所以,我们往往需要实现自定义的拒绝策略, 通过实现RejectedExecutionHandler接口的方式。


Java 线程和线程池

ThreadPoolExecutor的处理流程




三、线程池工作流程



了解了线程池中所有的重要属性之后,现在我们需要来了解下线程池的工作流程了。

Java 线程和线程池


整个过程可以拆分成以下几个部分:

1、提交任务

当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:创建一个工作线程来执行该任务、将任务加入阻塞队列、拒绝该任务。

提交任务的过程也可以拆分成以下几个部分:

  • 当工作线程数小于核心线程数时,直接创建新的核心工作线程

  • 当工作线程数不小于核心线程数时,就需要尝试将任务添加到阻塞队列中去

  • 如果能够加入成功,说明队列还没有满,那么需要做以下的二次验证来保证添加进去的任务能够成功被执行

  • 验证当前线程池的运行状态,如果是非RUNNING状态,则需要将任务从阻塞队列中移除,然后拒绝该任务

  • 验证当前线程池中的工作线程的个数,如果为0,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务

  • 如果加入失败,则说明队列已经满了,那么这时就需要创建新的“临时”工作线程来执行任务

  • 如果创建成功,则直接执行该任务

  • 如果创建失败,则说明工作线程数已经等于最大线程数了,则只能拒绝该任务了


整个过程可以用下面这张图来表示:


2、创建工作线程

创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。

首先,当线程池的状态是 SHUTDOWN 或者 STOP 时,则不能创建新的线程。另外,当线程工厂创建线程失败时,也不能创建新的线程。

还有就是当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。

除此之外,会尝试通过 CAS 来自增工作线程的个数,如果自增成功了,则会创建新的工作线程,即 Worker 对象。然后加锁进行二次验证是否能够创建工作线程,最后如果创建成功,则会启动该工作线程。


3、启动工作线程

当工作线程创建成功后,也就是 Worker 对象已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker 对象中关联着一个 Thread,所以要启动工作线程的话,只要通过 worker.thread.start() 来启动该线程即可。

启动完了之后,就会执行 Worker 对象的 run 方法,因为 Worker 实现了 Runnable 接口,所以本质上 Worker 也是一个线程。

通过线程 start 开启之后就会调用到 Runnable 的 run 方法,在 worker 对象的 run 方法中,调用了 runWorker(this) 方法,也就是把当前对象传递给了 runWorker 方法,让他来执行。


4、获取任务并执行

在 runWorker 方法被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而 Worker 对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行。

执行完了之后,就会去阻塞队列中获取任务来执行,而获取任务的过程,需要考虑当前工作线程的个数。

  • 如果工作线程数大于核心线程数,那么就需要通过 poll 来获取,因为这时需要对闲置的线程进行回收;

  • 如果工作线程数小于等于核心线程数,那么就可以通过 take 来获取了,因此这时所有的线程都是核心线程,不需要进行回收,前提是没有设置 allowCoreThreadTimeOut



四、线程池分类



线程池的返回值ExecutorService简介


  • ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程



具体的4种常用的线程池实现


  • 1-newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  • 2-newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  • 3-newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

  • 4-newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行;



五、线程池状态




线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。


1.RUNNING


状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!



2.SHUTDOWN


状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。


3.STOP


状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。


4.TIDYING


状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。


5.TERMINATED


状态说明:线程池彻底终止,就变成TERMINATED状态。

状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。




六、如何配置线程池的参数



前面提到创建线程池涉及到的几个参数,那么我们要如何设置这些参数才算是正确的应用呢?实际上,需要根据任务的特性来分析。


  • 任务的性质:CPU密集型、IO密集型和混杂型;

  • 任务的优先级:高中低;

  • 务执行的时间:长中短;

  • 任务的依赖性:是否依赖数据库或者其他系统资源


不同的性质的任务,我们采取的配置将有所不同。


通常来说,如果任务属于CPU密集型,那么我们可以将线程池数量设置成CPU的个数,以减少线程切换带来的开销。如果任务属于IO密集型,我们可以将线程池数量设置得更多一些,比如CPU个数*2。


PS:我们可以通过Runtime.getRuntime().availableProcessors()来获取CPU的个数。