1、无处不在的线程和进程

1.1 进程的基本原理

进程的定义一直以来没有完美的标准。一般来说,一个进程由程序段、数据段和进程控制块三部分组成。

image.png

程序段一般也被称为代码段。代码段是进程的程序指令在内存中的位置,包含需要执行的指令集合;数据段是进程的操作数据在内存中的位置,包含需要操作的数据集合;程序控制块(Program Control Block,PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志。

PCB主要由四大部分组成:

  1. 进程的描述信息。主要包括:进程ID和进程名称,进程ID是唯一的,代表进程的身份;进程状态,比如运行、就绪、阻塞;进程优先级,是进程调度的重要依据。
  2. 进程的调度信息。主要包括:程序起始地址,程序的第一行指令的内存地址,从这里开始程序的执行;通信信息,进程间通信时的消息队列。
  3. 进程的资源信息。主要包括:内存信息,内存占用情况和内存管理所用的数据结构;I/O设备信息,所用的I/O设备编号及相应的数据结构;文件句柄,所打开文件的信息。
  4. 进程上下文。主要包括执行时各种CPU寄存器的值、当前程序计数器(PC)的值以及各种栈的值等,即进程的环境。在操作系统切换进程时,当前进程被迫让出CPU,当前进程的上下文就保存在PCB结构中,供下次恢复运行时使用。

Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。在这个JVM进程内部,所有Java程序代码都是以线程来运行的。JVM找到程序的入口点main()方法,然后运行main()方法,这样就产生了一个线程,这个线程被称为主线程。当main()方法结束后,主线程运行完成,JVM进程也随即退出。

1.2 线程的基本原理

线程是指“进程代码段”的一次顺序执行流程。线程是CPU调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。
Java程序的进程执行过程就是标准的多线程的执行过程。每当使用Java命令执行一个class类时,实际上就是启动了一个JVM进程。理论上,在该进程的内部至少会启动两个线程,一个是main线程,另一个是GC(垃圾回收)线程。实际上,执行一个Java程序后,通过Process Explorer来观察,线程数量远远不止两个,达到了18个之多。
一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存
image.png

在线程的结构中,线程描述信息即线程的基本信息,主要包括:

  1. 线程ID(Thread ID,线程标识符)。线程的唯一标识,同一个进程内不同线程的ID不会重叠。
  2. 线程名称。主要是方便用户识别,用户可以指定线程的名字,如果没有指定,系统就会自动分配一个名称。
  3. 线程优先级。表示线程调度的优先级,优先级越高,获得CPU的执行机会就越大。
  4. 线程状态。表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
  5. 其他。例如是否为守护线程等,后面会详细介绍。

在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段内存地址。
在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB大小的栈内存。栈内存和堆内存不同,栈内存不受垃圾回收器管理。

1.3 进程与线程的区别

下面总结一下进程与线程的区别,主要有以下几点:

  1. 线程是“进程代码段”的一次顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。
  2. 线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
  3. 线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥了CPU的计算性能,又弥补了进程调度过于笨重的问题。
  4. 进程之间是相互独立的,但进程内部的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
  5. 切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称为轻量级进程。

1.4 创建线程的4种方法

1.4.1 Thread类详解

Thread 类的结构
image.png
介绍Thread类中比较重要的属性和方法
1.线程ID:
属性:private long tid,此属性用于保存线程的ID。这是一个private类型的属性,外部只能使用getId()方法访问线程的ID。
方法:public long getId(),获取线程ID,线程ID由JVM进行管理,在进程内唯一。比如,1.2节的实例中,所输出的main线程的ID为1。
2.线程名称
属性:private String name,该属性保存一个Thread线程实例的名字。
方法一:public final String getName(),获取线程名称。
方法二:public final void setName(String name),设置线程名称。
方法三:Thread(String threadName),通过此构造方法给线程设置一个定制化的名字。
3.线程优先级
属性:private int priority,保存一个Thread线程实例的优先级。
方法一:public final int getPriority(),获取线程优先级。
方法二:public final void setPriority(int priority),设置线程优先级。
Java线程的最大优先级值为10,最小值为1,默认值为5。这三个优先级值为三个常量值,在Thread类中使用类常量定义,三个类常量如下:

public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;

4.是否为守护线程
属性:private boolean daemon=false,该属性保存Thread线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程。
方法:public final void setDaemon(boolean on),将线程实例标记为守护线程或用户线程,如果参数值为true,那么将线程实例标记为守护线程。
5.线程的状态
属性:private int threadStatus,该属性以整数的形式保存线程的状态。
方法:public Thread.State getState(),返回表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
Thread的内部静态枚举类State用于定义Java线程的所有状态,具体如下:

public static enum State {
    NEW,                           //新建
    RUNNABLE,              //就绪、运行
    BLOCKED,               //阻塞
    WAITING,               //等待
    TIMED_WAITING,   //计时等待
    TERMINATED;            //结束
}

6.线程的启动和运行
方法一:public void start(),用来启动一个线程,当调用start()方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配需要的资源。
方法二:public void run(),作为线程代码逻辑的入口方法。run()方法不是由用户程序来调用的,当调用start()方法启动一个线程之后,只要线程获得了CPU执行时间,便进入run()方法体去执行具体的用户线程代码。
7.取得当前线程
方法:public static Thread currentThread(),该方法是一个非常重要的静态方法,用于获取当前线程的Thread实例对象。什么是当前线程呢?就是当前在CPU上执行的线程。在没有其他的途径获取当前线程的实例对象的时候,可以通过Thread.currentThread()静态方法获取。

1.4.2 继承Thread类创建线程类

通过前面的空线程例子可以看出,新线程如果需要并发执行自己的代码,需要做以下两件事情:

  1. 需要继承Thread类,创建一个新的线程类。
  2. 同时重写run()方法,将需要并发执行的业务代码编写在run()方法中。
public class CreateDemo {
    public static final int MAX_TURN = 5;
    public static String getCurThreadName() {
        return Thread.currentThread().getName();
    }
    //线程的编号
    static int threadNo = 1;

    static class DemoThread extends Thread {  //① 
        public DemoThread() {
            super("DemoThread-" + threadNo++); //②
        }

        public void run() {   //③
            for (int i = 1; i < MAX_TURN; i++) {
                Print.cfo(getName() + ", 轮次:" + i);
            }
            Print.cfo(getName() + " 运行结束.");
        }
    }

    public static void main(String args[]) throws InterruptedException {
        Thread thread = null;
        //方法一:使用Thread子类创建和启动线程
        for (int i = 0; i < 2; i++) {
            thread = new DemoThread();
            thread.start();
        }

        Print.cfo(getCurThreadName() + " 运行结束.");
    }
}
1.4.3 实现Runnable接口创建线程目标类

通过继承Thread类并重写它的run()方法只是创建Java线程的一种方式
Runnable是一个极为简单的接口,位于java.lang包中。接口中只有一个方法run(),具体的源代码如下:

package java.lang;
@FunctionalInterface
    public interface Runnable {
        void run();
    }

Runnable有且仅有一个抽象方法——void run(),代表被执行的用户业务逻辑的抽象,在使用的时候,将用户业务逻辑编写在Runnable实现类的run()的实现版本中。当Runnable实例传入Thread实例的target属性后,Runnable接口的run()的实现版本将被异步调用。

通过实现Runnable接口创建线程类
创建线程的第二种方法就是实现Runnable接口,将需要异步执行的业务逻辑代码放在Runnable实现类的run()方法中,将Runnable实例作为target执行目标传入Thread实例。该方法的具体步骤如下:

  1. 定义一个新类实现Runnable接口。
  2. 实现Runnable接口中的run()抽象方法,将线程代码逻辑存放在该run()实现版本中。
  3. 通过Thread类创建线程对象,将Runnable实例作为实际参数传递给Thread类的构造器,由Thread构造器将该Runnable实例赋值给自己的target执行目标属性。
  4. 调用Thread实例的start()方法启动线程。
  5. 线程启动之后,线程的run()方法将被JVM执行,该run()方法将调用target属性的run()方法,从而完成Runnable实现类中业务代码逻辑的并发执行。
public class CreateDemo2
    {
        public static final int MAX_TURN = 5;
        static int threadNo = 1;
        static class RunTarget implements Runnable  //①实现Runnable接口
            {
                public void run()  //②在这里编写业务逻辑
                {
                    for (int j = 1; j < MAX_TURN; j++)
                        {
                            Print.cfo(ThreadUtil.getCurThreadName() + ", 轮次:" + j);
                        }
                    Print.cfo(getCurThreadName() + " 运行结束.");
                }
            }

        public static void main(String args[]) throws InterruptedException
        {
            Thread thread = null;
            for (int i = 0; i < 2; i++)
                {
                    Runnable target = new RunTarget();
                    //通过Thread 类创建线程对象,将Runnable实例作为实际参数传入
                    thread = new Thread(target, "RunnableThread" + threadNo++);
                    thread.start();
                }
        }
    }

1.4.4 使用Callable和FutureTask创建线程

前面已经介绍了继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。
这是一个比较大的问题,很多场景都需要获取异步执行的结果,通过Runnable无法实现,是因为它的run()方法不支持返回值。
为了解决异步执行的结果问题,Java语言在1.5版本之后提供了一种新的多线程创建方法:通过Callable接口和FutureTask类相结合创建线程。
Callable接口
Callable接口位于java.util.concurrent包中,查看它的Java源代码

package java.util.concurrent;
@FunctionalInterface
    public interface Callable<V> {
        V call() throws Exception;
    }

RunnableFuture接口

RunnableFuture是如何在Callable与Thread之间实现搭桥功能的呢?RunnableFuture接口实现了两个目标:一是可以作为Thread线程实例的target实例,二是可以获取异步执行的结果。它是如何做到一箭双雕的呢?请看RunnableFuture接口的代码:

package java.util.concurrent;
     
     public interface RunnableFuture<V>  extends  Runnable, Future<V> {
         void run();
     }

Future接口
Future接口至少提供了三大功能:

  1. 能够取消异步执行中的任务。
  2. 判断异步任务是否执行完成。
  3. 获取异步任务完成后的执行结果。

Future接口的源代码如下:

    package java.util.concurrent;
     public interface Future<V> {
         boolean cancel(boolean mayInterruptRunning); //取消异步执行
         boolean isCancelled();
         boolean isDone();//判断异步任务是否执行完成
         //获取异步任务完成后的执行结果
         V get() throws InterruptedException, ExecutionException;
         //设置时限,获取异步任务完成后的执行结果
         V get(long timeout, TimeUnit unit) throws InterruptedException, 
                                               ExecutionException, TimeoutException;
        ...
     }

Callable和FutureTask创建线程的具体步骤

  1. 创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
  2. 使用Callable实现类的实例构造一个FutureTask实例。
  3. 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
  4. 调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
  5. 调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。
public class CreateDemo3 {
         public static final int MAX_TURN = 5;
         public static final int COMPUTE_TIMES = 100000000;
     
     //①创建一个 Callable 接口的实现类
         static class ReturnableTask implements Callable<Long> {
              //②编写好异步执行的具体逻辑,可以有返回值
              public Long call() throws Exception{
                 long startTime = System.currentTimeMillis();
                 Print.cfo(getCurThreadName() + " 线程运行开始.");
                 Thread.sleep(1000);
     
                 for (int i = 0; i < COMPUTE_TIMES; i++) {
                     int j = i * 10000;
                 }
                 long used = System.currentTimeMillis() - startTime;
                 Print.cfo(getCurThreadName() + " 线程运行结束.");
                 return used;
             }
         }
     
         public static void main(String args[]) throws InterruptedException {
             ReturnableTask task=new ReturnableTask();//③
             FutureTask<Long> futureTask = new FutureTask<Long>(task);//④
             Thread thread = new Thread(futureTask, "returnableThread");//⑤
             thread.start();//⑥
             Thread.sleep(500);
             Print.cfo(getCurThreadName() + " 让子弹飞一会儿.");
             Print.cfo(getCurThreadName() + " 做一点自己的事情.");
             for (int i = 0; i < COMPUTE_TIMES / 2; i++) {
                 int j = i * 10000;
             }
     
             Print.cfo(getCurThreadName() + " 获取并发任务的执行结果.");
             try {
                 Print.cfo(thread.getName()+"线程占用时间:"
                                          + futureTask.get());//⑦
             } catch (InterruptedException e) {
                 e.printStackTrace();
             } catch (ExecutionException e) {
                 e.printStackTrace();
             }
             Print.cfo(getCurThreadName() + " 运行结束.");
         }
     }
1.4.5 通过线程池创建线程

线程池的创建与执行目标提交
通过Executors工厂类创建一个线程池,一个简单的示例如下:

//创建一个包含三个线程的线程池
private static ExecutorService pool = Executors.newFixedThreadPool(3);

ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。
向ExecutorService线程池提交异步执行target目标任务的常用方法有:

//方法一:执行一个 Runnable类型的target执行目标实例,无返回
void execute(Runnable command);

//方法二:提交一个 Callable类型的target执行目标实例, 返回一个Future异步任务实例
<T> Future<T> submit(Callable<T> task);  

//方法三:提交一个 Runnable类型的target执行目标实例, 返回一个Future异步任务实例
Future<?> submit(Runnable task);

线程池的使用实战
使用Executors创建线程池,然后使用ExecutorService线程池执行或者提交target执行目标实例的示例代码:

public class CreateDemo4
    {

        public static final int MAX_TURN = 5;
        public static final int COMPUTE_TIMES = 100000000;

        //创建一个包含三个线程的线程池
        private static ExecutorService pool = Executors.newFixedThreadPool(3);

        static class DemoThread implements Runnable
            {
                @Override
                public void run()
                {
                    for (int j = 1; j < MAX_TURN; j++)
                        {
                            Print.cfo(getCurThreadName() + ", 轮次:" + j);
                            sleepMilliSeconds(10);
                        }
                }
            }


        static class ReturnableTask implements Callable<Long>
            {
                //返回并发执行的时间
                public Long call() throws Exception
                {
                    long startTime = System.currentTimeMillis();
                    Print.cfo(getCurThreadName() + " 线程运行开始.");
                    for (int j = 1; j < MAX_TURN; j++)
                        {
                            Print.cfo(getCurThreadName() + ", 轮次:" + j);
                            sleepMilliSeconds(10);
                        }
                    long used = System.currentTimeMillis() - startTime;
                    Print.cfo(getCurThreadName() + " 线程运行结束.");
                    return used;
                }
            }

        public static void main(String[] args) {

            pool.execute(new DemoThread()); //执行线程实例,无返回
            pool.execute(new Runnable()
                         {
                             @Override
                             public void run()
                             {
                                 for (int j = 1; j < MAX_TURN; j++)
                                     {
                                         Print.cfo(getCurThreadName() + ", 轮次:" + j);
                                         sleepMilliSeconds(10);
                                     }
                             }
                         });
            //提交Callable 执行目标实例,有返回
            Future future = pool.submit(new ReturnableTask());
            Long result = (Long) future.get();
            Print.cfo("异步任务的执行结果为:" + result);
            sleepSeconds(Integer.MAX_VALUE);
        }
    }

1.4.6 线程间状态切换

image.png

1.NEW状态
通过new Thread(…)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式本质上都是通过new Thread()创建线程,仅仅是创建了不同的target执行目标实例(如Runnable实例)。

2.RUNNABLE状态
Java把Ready(就绪)和Running(执行)两种状态合并为一种状态:RUNNABLE(可执行)状态(或者可运行状态)。调用了线程的start()实例方法后,线程就处于就绪状态。此线程获取到CPU时间片后,开始执行run()方法中的业务代码,线程处于执行状态。
(1)就绪状态
就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远处于就绪状态。当前线程进入就绪状态的条件大致包括以下几种:
·调用线程的start()方法,此线程就会进入就绪状态。
·当前线程的执行时间片用完。
·线程睡眠(Sleep)操作结束。
·对其他线程合入(Join)操作结束。
·等待用户输入结束。
·线程争抢到对象锁(Object Monitor)。
·当前线程调用了yield()方法出让CPU执行权限。
(2)执行状态
线程调度程序从就绪状态的线程中选择一个线程,被选中的线程状态将变成执行状态。这也是线程进入执行状态的唯一方式。

3.BLOCKED状态
处于BLOCKED(阻塞)状态的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁
等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2)IO阻塞
线程发起了一个阻塞式IO操作后,如果不具备IO操作的条件,线程就会进入阻塞状态。IO包括磁盘IO、网络IO等。IO阻塞的一个简单例子:线程等待用户输入内容后继续执行。

4.WAITING状态
处于WAITING(无限期等待)状态的线程不会被分配CPU时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下3种方法会让自己进入无限等待状态:
·Object.wait()方法,对应的唤醒方式为:Object.notify()/Object.notifyAll()。
·Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕。
·LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)。

5.TIMED_WAITING状态
处于TIMED_WAITING(限时等待)状态的线程不会被分配CPU时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下3种方法会让线程进入限时等待状态:
·Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束。
·Object.wait(time)方法,对应的唤醒方式为:调用Object.notify()/Object.notifyAll()主动唤醒,或者限时结束。
·LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。
进入BLOCKED状态、WAITING状态、TIMED_WAITING状态的线程都会让出CPU的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入Ready状态,需要重新获取时间片才能接着运行。

6.TERMINATED状态
线程结束任务之后,将会正常进入TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

2、线程池架构

2.1 介绍

在web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理。如果每次请求都新创建一个线程的话实现起来非常简便,但是存在一个问题:
如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来会大大降低系统的效率。可能出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多。
那么有没有一种办法使执行完一个任务,并不被销毁,而是可以继续执行其他的任务呢?
这就是线程池的目的了。线程池为线程生命周期的开销和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上

2.2 什么时候使用线程池?

  • 单个任务处理时间比较短
  • 需要处理的任务数量很大

2.3 线程池优势

  • 重用存在的线程,减少线程创建,消亡的开销,提高性能
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.4 线程池主要解决的问题

线程池主要解决两个问题:

  • 一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接 new 一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。二是线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。每个 ThreadPoolExecutor 也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
  • 另外,线程池也提供了许多可调参数和可扩展性接口,以满足不同情境的需要,程序员可以使用更方便的 Executors 的工厂方法,比如 newCachedThreadPool(线程池线程个数最多可达 Integer.MAX_VALUE,线程自动回收)、newFixedThreadPool(固定大小的线程池)和 newSingleThreadExecutor(单个线程)等来创建线程池,当然用户还可以自定义。

2.5 线程池架构

image.png
1.Executor
Executor是Java异步目标任务的“执行者”接口,其目标是执行目标任务。“执行者”Executor提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的是提供一种将“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

void execute(Runnable command)

2.ExecutorService
ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务接”口,对外提供异步任务的接收服务。ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系列方法、invoke系列方法等,具体如下:

 //向线程池提交单个异步任务
     <T> Future<T> submit(Callable<T> task);
     //向线程池提交批量异步任务
     <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
             throws InterruptedException;

3.AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现。

4.ThreadPoolExecutor

ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽象类。

ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

5.ScheduledExecutorService
ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时”和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。
6.ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor的性能要优于Timer。

7.Executors
Executors是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

3、Executors的4种快捷创建线程池的方法

3.1 newSingleThreadExecutor创建“单线程化线程池”

该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
调用Executors.newSingleThreadExecutor()快捷工厂方法创建一个“单线程化线程池”的测试用例:

public class CreateThreadPoolDemo
    {
        public static final int SLEEP_GAP = 500;
        //异步任务的执行目标类
        static class TargetTask implements Runnable
            {
                static AtomicInteger taskNo = new AtomicInteger(1);
                private String taskName;
                public TargetTask()
                {
                    taskName = "task-" + taskNo.get();
                    taskNo.incrementAndGet();
                }
                public void run()
                {
                    Print.tco("任务:" + taskName + " doing");
                    // 线程睡眠一会
                    sleepMilliSeconds(SLEEP_GAP);
                    Print.tco(taskName + " 运行结束.");
                }
            }

        //测试用例:只有一个线程的线程池
        @Test
        public void testSingleThreadExecutor()
        {
            ExecutorService pool = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 5; i++)
                {
                    pool.execute(new TargetTask());
                    pool.submit(new TargetTask());
                }
            sleepSeconds(1000);
            //关闭线程池
            pool.shutdown();
        }
    }

总结:

  1. 单线程化的线程池中的任务是按照提交的次序顺序执行的。
  2. 池中的唯一线程的存活时间是无限的。
  3. 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。

总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

3.2 newFixedThreadPool创建“固定数量的线程池”

public class CreateThreadPoolDemo
     {
         public static final int SLEEP_GAP = 500;
         //异步任务的执行目标类
         static class TargetTask implements Runnable
         {
         //为了节约篇幅,省略重复内容
         }
        //测试用例:只有3个线程固定大小的线程池
         @Test
         public void testNewFixedThreadPool()
         {
             ExecutorService pool = Executors.newFixedThreadPool(3);
             for (int i = 0; i < 5; i++)
             {
                 pool.execute(new TargetTask());
                 pool.submit(new TargetTask());
             }
             sleepSeconds(1000);
             //关闭线程池
             pool.shutdown();
         }
         // 省略其他
     }

3.2.1 “固定数量的线程池”的特点大致如下:
  1. 如果线程数没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
  2. 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  3. 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
3.2.2 “固定数量的线程池”的适用场景:

需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定地保证一个数,能够避免频繁回收线程和创建线程,故适用于处理CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能少地分配线程。

3.2.3 “固定数量的线程池”的弊端:

内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

3.3 newCachedThreadPool创建“可缓存线程池”

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。
调用Executors.newCachedThreadPool()快捷工厂方法创建一个“可缓存线程池”的测试用例

public class CreateThreadPoolDemo
     {
         public static final int SLEEP_GAP = 500;
         //异步任务的执行目标类
         static class TargetTask implements Runnable
         {
         //为了节约篇幅,省略重复内容
         }
            //测试用例:“可缓存线程池”
             @Test
             public void testNewCacheThreadPool()
             {
                 ExecutorService pool = Executors.newCachedThreadPool();
                 for (int i = 0; i < 5; i++)
                 {
                     pool.execute(new TargetTask());
                     pool.submit(new TargetTask());
                 }
                 sleepSeconds(1000);
                 //关闭线程池
                 pool.shutdown();
             }
         // 省略其他
     }

3.3.1 “可缓存线程池”的特点大致如下:

(1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
(2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
(3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。

3.3.2 “可缓存线程池”的适用场景:

需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。

3.3.3 “可缓存线程池”的弊端:

线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

3.4 newScheduledThreadPool创建“可调度线程池”

该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。Executors提供了多个创建“可调度线程池”的工厂方法,部分如下:

//方法一:创建一个可调度线程池,池内仅含有一个线程
     public static ScheduledExecutorService newSingleThreadScheduledExecutor();
     
     //方法二:创建一个可调度线程池,池内含有N个线程,N的值为输入参数corePoolSize
     public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;

newSingleThreadScheduledExecutor工厂方法所创建的仅含有一个线程的可调度线程池适用于调度串行化任务,也就是一个任务一个任务地串行化调度执行。调用Executors.newScheduledThreadPool(int corePoolSize)快捷工厂方法创建一个“可调度线程池”的测试用例,其代码如下:

public class CreateThreadPoolDemo
     {
         public static final int SLEEP_GAP = 500;
         //异步任务的执行目标类
         static class TargetTask implements Runnable
         {
         //为了节约篇幅,省略重复内容
         }
     
         //测试用例:“可调度线程池”
         @Test
         public void testNewScheduledThreadPool()
         {
              ScheduledExecutorService scheduled = 
                                Executors.newScheduledThreadPool(2);
              for (int i = 0; i < 2; i++)
             {
                 scheduled.scheduleAtFixedRate(new TargetTask(),
                         0, 500, TimeUnit.MILLISECONDS);
                 // 以上参数中:0表示首次执行任务的延迟时间,500表示每次执行任务的间隔时间
                 // TimeUnit.MILLISECONDS是执行的时间间隔数值,单位为毫秒
             }
             sleepSeconds(1000);
             //关闭线程池
             scheduled.shutdown();
         }
     // 省略其他
     }