Java高并发核心编程读书笔记(一)

178 阅读32分钟

说明:该笔记来自于《Java高并发核心编程》

一 进程与线程

程是资源分配的最小单位,线程是CPU调度的最小单位

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

1.1 进程

1.1.1 基本概念

  • 程序是存放在硬盘中的可执行文件,主要包括代码指令和数据。
  • 一个进程是一个程序的一次启动和执行,是操作系统将程序装入内存,给程序分配必要的系统资源,并且开始运行程序的指令,进程是资源分配的最小单位。
  • 在我们的window的任务管理器中那个中我们可以发现许多进程。

1.1.2 运行原理

般来说,一个进程由程序段、数据段和进程控制块三部分组成

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

1.2 线程

  • 线程是指进程代码段的一次顺序执行流程。
  • 线程是CPU调度的最小单位。
  • 一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。

程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

栈内存

  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程(方法,方法基本元素)。
  • 每个线程运行时所需要的内存,称为虚拟机栈。
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
  • 方法的每一次执行都需要为其分配一个栈帧(方法帧),栈帧主要保存该方法中的局部变量、方法的返回地址以及其他方法的相关信息
  • 当线程的执行流程进入方法时,JVM就会为方法分配一个对应的栈帧压入栈内存;当线程的执行流程跳出方法时,JVM就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。

线程基本信息

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

1.3 创建线程

1.3.1 Thread类详解

  • 线程ID
private long tid;// 线程id属性
public long getId() // 获取线程id   
  • 线程名称
private String name;// 线程名称
public final String getName();//获取线程名称
public final void setName(String name);// 设置线程名称
Thread(String threadName);//构造线程名称
  • 线程优先级
private int priority;//保存一个Thread线程实例的优先级
public final int getPriority();//获取线程优先级。
public final void setPriority(int priority);//设置线程优先级
// Java线程的最大优先级值为10,最小值为1,默认值为5
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
  • 是否守护线程
private boolean daemon=false//该属性保存Thread线程实例的守护状态,默认为false,表示是普通的用户线程,而不是守护线程
public final void setDaemon(boolean on);//将线程实例标记为守护线程或用户线程,如果参数值为true,那么将线程实例标记为守护线程
  • 线程状态
private int threadStatus;//该属性以整数的形式保存线程的状态
public Thread.State getState();//返回表示当前线程的执行状态,为新建、就绪、运行、阻塞、结束等状态中的一种。
public static enum State {
             NEW,                           //新建
             RUNNABLE,              //就绪、运行
             BLOCKED,               //阻塞
             WAITING,               //等待
             TIMED_WAITING,   //计时等待
             TERMINATED;            //结束
     }
  • 启动与运行
public void start();//用来启动一个线程,当调用start()方法后,JVM才会开启一个新的线程来执行用户定义的线程代码逻辑,在这个过程中会为相应的线程分配需要的资源
public void run();//作为线程代码逻辑的入口方法。run()方法不是由用户程序来调用的,当调用start()方法启动一个线程之后,只要线程获得了CPU执行时间,便进入run()方法体去执行具体的用户线程代码。
  • 当前线程
public static Thread currentThread();//该方法是一个非常重要的静态方法,用于获取当前线程的Thread实例对象。

1.3 .2 简单实现

第一种方式:集成Thread

package com.Thread;
​
/**
 * @Author shu
 * @Date: 2021/11/30/ 10:59
 * @Description 线程的Thread创建方式
 **/
public class SimpleThread extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("执行方法"+currentThread().getName());
    }
​
    public static void main(String[] args) {
        new SimpleThread().start();
    }
}

第二种方式:实现Runnable

package com.Thread;

/**
 * @Author shu
 * @Date: 2021/11/30/ 13:55
 * @Description 简单Runnable实现方式
 **/
public class SimpleRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            System.out.println("执行方法"+Thread.currentThread().getName()+i);
        }

    }

    public static void main(String[] args) {
        SimpleRunnable runnable = new SimpleRunnable();
        new Thread(runnable).start();


        //Lambda简写
        Runnable runs=(()->{
            for (int i = 0; i <10 ; i++) {
                System.out.println("执行方法"+Thread.currentThread().getName()+i);
            }
        });
    }
}

第三种:Callable和FutureTask创建线程

package com.Thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @Author shu
 * @Date: 2021/11/30/ 14:00
 * @Description 简单的Callable实现
 **/
public class SimpleCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask futureTask=new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                int sum = 0 ;
                for(int i = 1 ; i <= 10 ; i++ ){
                    System.out.println(Thread.currentThread().getName()+" => " + i);
                    sum+=i;
                }
                return Thread.currentThread().getName()+"执行的结果是:"+sum;
            }
        });
        futureTask.run();
        System.out.println(futureTask.get());
    }
}

Future接口

Future接口至少提供了三大功能:(1)能够取消异步执行中的任务。(2)判断异步任务是否执行完成。(3)获取异步任务完成后的执行结果。

     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;
     }

RunnableFuture接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

RunnableFuture继承了Runnable接口,从而保证了其实例可以作为Thread线程实例的target目标;同时,RunnableFuture通过继承Future接口,保证了可以获取未来的异步执行结果。

FutureTask实现类

FutureTask类是Future接口的实现类,提供了对异步任务的操作的具体实现。但是,FutureTask类不仅实现了Future接口,还实现了Runnable接口,或者更加准确地说,FutureTask类实现了RunnableFuture接口。

总结一下

  • Thread与Runnable其实是一致的,Thread实现了Runnable接口,唯一的缺点是无法接受线程返回的值
  • Callable通过一个未来任务对象,来接受线程返回的值,两者适用于不同的环境

线程池创建线程

实际上创建一个线程实例在时间成本、资源耗费上都很高(稍后会介绍),在高并发的场景中,断然不能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。(线程复用)

  • ExecutorService是Java提供的一个线程池接口,每次我们在异步执行target目标任务的时候,可以通过ExecutorService线程池实例去提交或者执行。
  • ExecutorService实例负责对池中的线程进行管理和调度,并且可以有效控制最大并发线程数,提高系统资源的使用率,同时提供定时执行、定频执行、单线程、并发数控制等功能。
     //创建一个包含三个线程的线程池
     private static ExecutorService pool = Executors.newFixedThreadPool(3);
      //方法一:执行一个 Runnable类型的target执行目标实例,无返回
     void execute(Runnable command);
     //方法二:提交一个 Callable类型的target执行目标实例, 返回一个Future异步任务实例
     <T> Future<T> submit(Callable<T> task);                  
     //方法三:提交一个 Runnable类型的target执行目标实例, 返回一个Future异步任务实例
     Future<?> submit(Runnable task);

1.4 线程的核心原理

1.4.1 线程调度与时间片

时间片

  • 由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。

线程调度

  • 目前操作系统中主流的线程调度方式是:基于CPU时间片方式进行线程调度。
  • 线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。
  • 由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在“同时执行”或者并发执行。
  • 线程的调度模型目前主要分为两种:分时调度模型(平均分配)抢占式调度模型(优先级别)

线程优先级请看前面内容

1.4.2 线程状态

  • 线程的生命周期分为新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)这5种状态。
  • 新建(New): 在Java中使用new关键字创建一个线程,新创建的线程将处于新建状态。
  • 就绪状态(Runnable):新建的线程对象在调用start方法之后将转为就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状态。
  • 运行状态(Running):就绪状态的线程在竞争到CPU的使用权并开始执行run方法的线程执行体时,会转为运行状态。
  • 阻塞状态(Blocked):运行中的线程会主动或被动地放弃CPU的使用权并暂停运行,此时该线程将转为阻塞状态。
  • 线程死亡(Dead):正常退出,或异常退出。

1.4.3 线程方法

  • 线程等待:调用wait方法的线程会进入WAITING状态,只有等到其他线程的通知或被中断后才会返回。
  • 线程睡眠:调用sleep方法会导致当前线程休眠。与wait方法不同的是,sleep方法不会释放当前占有的锁,会导致线程进入TIMED-WATING状态,而wait方法会导致当前线程进入WATING状态。
     //使目前正在执行的线程休眠millis毫秒
     public static void sleep(long millis) throws InterruptException;
     //使目前正在执行的线程休眠millis毫秒,nanos纳秒
     public static void sleep(long millis,int nanos) throws InterruptException;
  • 线程让步:调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞争CPU时间片。(运行状态转为就绪状态)
  • 线程中断:interrupt方法用于向线程发行一个终止通知信号,会影响该线程内部的一个中断标识位,这个线程本身并不会因为调用了interrupt方法而改变状态(阻塞、终止等)。
  • 线程加入:join方法用于等待其他线程终止,如果在当前线程中调用一个线程的join方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态转为就绪状态,等待获取CPU的使用权。
     //重载版本1:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束
     public final void join() throws InterruptedException:
     //重载版本2:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis的时间
     public final synchronized void join(long millis) throws InterruptedException:
     //重载版本3:此方法会把当前线程变为TIMED_WAITING,直到被合并线程执行结束,或者等待被合并线程执行millis+nanos的时间
     public final synchroinzed void join(long millis, int nanos) throws InterruptedException:
  • 线程唤醒:Object类有个notify方法,用于唤醒在此对象监视器上等待的一个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的。
  • 后台守护线程:setDaemon方法用于定义一个守护线程,也叫作服务线程,该线程是后台线程,有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。

1.4.4 线程状态总结

New状态

通过new Thread(…)已经创建线程,但尚未调用start()启动线程,该线程处于NEW(新建)状态。

RUNNABLE状态

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

BLOCKED状态

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

WAITING状态

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

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状态,需要重新获取时间片才能接着运行。

TERMINATED状态

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

1.5 线程池原理

1.5.1 4种线程池的创建

1.5.1.1 newSingleThreadExecutor

创建一个单线程化线程池,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。

package ExecutorDemo.newSingleThreadExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/30 17:32
 * @version: 1.0
 */
public class SingleThreadExecutorDemo {

    public static final int SLEEP_FLAG = 500;

    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 主方法
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        // 线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        // 批量添加线程
        for (int i = 0; i < 5; i++) {

            singleThreadExecutor.execute(new TargetTask());

          //  singleThreadExecutor.submit(new TargetTask());
        }
        Thread.sleep(1000);
        // 线程池销毁
        singleThreadExecutor.shutdown();;
    }

}

特点

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

使用场景

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

1.5.1.2 newFixedThreadPool

package ExecutorDemo.newFixedThreadPool;

import ExecutorDemo.newSingleThreadExecutor.SingleThreadExecutorDemo;

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

/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/30 17:55
 * @version: 1.0
 */
public class FixedThreadPoolDemo {


    public static final int SLEEP_FLAG = 500;

    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 主方法
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        // 线程池
        ExecutorService singleThreadExecutor = Executors.newFixedThreadPool(2);
        // 批量添加线程
        for (int i = 0; i < 7; i++) {

            singleThreadExecutor.execute(new TargetTask());

            //  singleThreadExecutor.submit(new TargetTask());
        }
        Thread.sleep(1000);
        // 线程池销毁
        singleThreadExecutor.shutdown();;
    }
}

特点

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

使用场景

固定数量的线程池的适用场景:需要任务长期执行的场景。但是内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

1.5.1.3 newCachedThreadPool

package ExecutorDemo.newCachedThreadPool;

import ExecutorDemo.newFixedThreadPool.FixedThreadPoolDemo;

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

/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/30 18:35
 * @version: 1.0
 */
public class CachedThreadPoolDemo {
    public static final int SLEEP_FLAG = 500;

    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 主方法
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        // 线程池
        ExecutorService singleThreadExecutor = Executors.newCachedThreadPool();
        // 批量添加线程
        for (int i = 0; i < 7; i++) {

            singleThreadExecutor.execute(new TargetTask());

            //  singleThreadExecutor.submit(new TargetTask());
        }
        Thread.sleep(1000);
        // 线程池销毁
        singleThreadExecutor.shutdown();;
    }
}

特点

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

使用场景

可缓存线程池”的适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景。但是线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

1.5.1.4 newScheduledThreadPool

该方法用于创建一个可调度线程池,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池

package ExecutorDemo.newScheduledThreadPool;

import ExecutorDemo.newFixedThreadPool.FixedThreadPoolDemo;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/30 19:54
 * @version: 1.0
 */
public class ScheduledThreadPoolDemo {

    public static final int SLEEP_FLAG = 500;

    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * 主方法
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        // 线程池
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        // 批量添加线程
        for (int i = 0; i < 7; i++) {
            ScheduledFuture<?> future = service.scheduleWithFixedDelay(new TargetTask(), 0, 500, TimeUnit.MILLISECONDS);


        }
        Thread.sleep(1000);
        // 线程池销毁
        service.shutdown();;
    }
}

参数

     public ScheduledFuture<?> scheduleAtFixedRate(
             Runnable command,      //异步任务target执行目标实例
             long initialDelay,     //首次执行延时
             long period,           //两次开始执行最小间隔时间
             TimeUnit unit                  //所设置的时间的计时单位,如TimeUnit.SECONDS常量
             );

     public ScheduledFuture<?> scheduleWithFixedDelay(
         Runnable command,  //异步任务target执行目标实例
         long initialDelay,         //首次执行延时
         long delay,          //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间)
         TimeUnit unit        //所设置的时间的计时单位,如TimeUnit.SECONDS常量
     );

使用场景

可调度线程池的适用场景:周期性地执行任务的场景。Spring Boot中的任务调度器,底层借助了JUC的ScheduleExecutorService“可调度线程池”实现,并且可以通过@Configuration配置类型的Bean。

注意

尽管Executors的工厂方法使用方便,但是在生产场景中被很多企业(尤其是大厂)的开发规范所禁用。

1.5.2 线程池的标准创建

注意

  • 大部分企业的开发规范都会禁止使用快捷线程池(具体原因稍后介绍),要求通过标准构造器ThreadPoolExecutor去构造工作线程池。
  • Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。
     // 使用标准构造器构造一个普通的线程池
     public ThreadPoolExecutor(
       int corePoolSize,            // 核心线程数,即使线程空闲(Idle),也不会回收
       int maximumPoolSize,                 // 线程数的上限
       long keepAliveTime, TimeUnit unit,   // 线程最大空闲(Idle)时长 
       BlockingQueue<Runnable> BlockingQueue,     // 任务的排队队列
       ThreadFactory threadFactory,                         // 新线程的产生方式
       RejectedExecutionHandler handler)    // 拒绝策略

1.5.2 .1corePoolSize与maximumPoolSize

参数corePoolSize用于设置核心(Core)线程池数量,参数maximumPoolSize用于设置最大线程数量。

合理设置

参考文章:blog.csdn.net/riemann_/ar… CPU密集型:corePoolSize = CPU核数 + 1 IO密集型:corePoolSize = CPU核数 * 2

工作规则

  • 当在线程池接收到新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePoolSize。
  • 如果当前工作线程数多于corePoolSize数量,但小于maximumPoolSize数量,那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固定大小的线程池。
  • 当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
  • orePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize()和setMaximumPoolSize()两个方法进行动态更改。

1.5.2.2 BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。

1.5.2.3 keepAliveTime

线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收。

1.5.2.4 allowCoreThreadTimeout

但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。

1.5.2.5 RejectedExecutionHandler

在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:(1)线程池已经被关闭。(2)工作队列已满且maximumPoolSize已满。 AbortPolicy:拒绝策略。·DiscardPolicy:抛弃策略。·DiscardOldestPolicy:抛弃最老任务策略。·CallerRunsPolicy:调用者执行策略。·自定义策略。

1.5.3 两种提交方法的比较

  • Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。
  • Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果。
  • Runnable和Callable的主要区别为:Callable允许有返回值,Runnable不允许有返回值;Runnable不允许抛出异常,Callable允许抛出异常。
  • submit()提交任务后会有返回值,而execute()没有execute()方法主要用于启动任务的执行,而任务的执行结果和可能的异常调用者并不关心。
  • submit()方法也用于启动任务的执行,但是启动之后会返回Future对象,代表一个异步执行实例,可以通过该异步执行实例去获取结果。
  • execute()方法在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。
  • 通过submit()方法返回的Future对象(异步执行实例),可以进行异步执行过程中的异常捕获。

1.5.4 线程池调度的流程

  • 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
  • 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。
  • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
  • 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
  • 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。

1.5.5 线程工厂

ThreadFactory是Java线程工厂接口,这是一个非常简单的接口,具体如下:

     package java.util.concurrent;
     public interface ThreadFactory {
         //唯一的方法:创建一个新线程
         Thread newThread(Runnable target);
     }
  • 在调用ThreadFactory的唯一方法newThread()创建新线程时,可以更改所创建的新线程的名称、线程组、优先级、守护进程状态等。
  • 如果newThread()的返回值为null,表示线程工厂未能成功创建线程,线程池可能无法执行任何任务。
package ThreadFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 先建线程工厂
 * @author: shu
 * @createDate: 2022/10/31 16:01
 * @version: 1.0
 */
public class ThreadFactoryDemo {

    public static final int SLEEP_FLAG = 500;
    //一个简单的线程工厂
    static public class SimpleThreadFactory implements ThreadFactory
    {
        static AtomicInteger threadNo = new AtomicInteger(1);
        //实现其唯一的创建线程方法
        @Override
        public Thread newThread(Runnable target)
        {
            String threadName = "simpleThread-" + threadNo.get();

            System.out.println("创建一个线程,名称为:" + threadName);

            threadNo.incrementAndGet();
            //设置线程名称和异步执行目标
            Thread thread = new Thread(target,threadName);
            //设置为守护线程
            thread.setDaemon(true);
            return thread;
        }
    }


    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }




    public static void main(String[] args) throws InterruptedException {
        //使用自定义线程工厂快捷创建一个固定大小的线程池
        ExecutorService pool =
                Executors.newFixedThreadPool(2,new  SimpleThreadFactory());
        for (int i = 0; i < 5; i++)
        {
            pool.submit(new TargetTask());
        }
        //等待10秒
        Thread.sleep(1000);
        // 线程池销毁
        pool.shutdown();
    }

}




    // 省略其他

注意

Executors为线程池工厂类,用于快捷创建线程池(Thread Pool);ThreadFactory为线程工厂类,用于创建线程(Thread)。

1.5.6 阻塞队列

1.5.6.1 ArrayBlockingQueue

ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时设置的大小,若该阻塞队列已满,则会为新的任务创建线程,直到线程池中的线程总数大于maximumPoolSize。

1.5.6.2 LinkedBlockingQueue

LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。

1.5.6.3 PriorityBlockingQueue

是具有优先级的无界队列

1.5.6.4 DelayQueue

这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。

1.5.6.5 SynchronousQueue

(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue

1.5.7 调度器的钩子函数

ThreadPoolExecutor线程池调度器为每个任务执行前后都提供了钩子方法。

     //任务执行之前的钩子方法(前钩子)
     protected void beforeExecute(Thread t, Runnable r)   { }
     //任务执行之后的钩子方法(后钩子)
     protected void afterExecute(Runnable r, Throwable t) { }
     //线程池终止时的钩子方法(停止钩子)
     protected void terminated() { }
package ThreadFactory;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/31 16:52
 * @version: 1.0
 */
public class ThreadPoolExecutorDemo {

    public static final int SLEEP_FLAG = 500;


    /**
     * 静态类
     */
    static class TargetTask implements Runnable {

        // 计数器,原子类
        static AtomicInteger TaskNo = new AtomicInteger();
        public String taskName;

        /**
         * 构造器
         */
        public TargetTask() {
            this.taskName = "task-" + TaskNo.get();
            TaskNo.incrementAndGet();
        }

        /**
         * 线程入口
         */
        @Override
        public void run() {
            System.out.println("任务编号-" + taskName + "正在运行");
            try {
                Thread.sleep(SLEEP_FLAG);
                System.out.println("任务编号-" + taskName + "运行结束");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }





    public static void main(String[] args) throws InterruptedException {

        ExecutorService pool = new ThreadPoolExecutor(2,//coreSize
                4, //最大线程数
                60,//空闲保活时长
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2)) //等待队列
        {
            //继承:调度器终止钩子
            @Override
            protected void terminated()
            {
                System.out.println("调度器已经终止!");
            }

            //继承:执行前钩子
            @Override
            protected void beforeExecute(Thread t, Runnable target)
            {
                System.out.println("前钩被执行!");
                super.beforeExecute(t, target);
            }


            //继承:执行后钩子
            @Override
            protected void afterExecute(Runnable target, Throwable t)
            {
                System.out.println("钩子函数执行后");
            }
        };

        for (int i = 1; i <= 5; i++)
        {
            pool.execute(new TargetTask());
        }
        //等待10秒
        Thread.sleep(1000);
        // 线程池销毁
        pool.shutdown();
    }


}

1.5.8 线程的拒绝策越

在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:(1)线程池已经被关闭。(2)工作队列已满且maximumPoolSize已满。 ·AbortPolicy:拒绝策略。·DiscardPolicy:抛弃策略。·DiscardOldestPolicy:抛弃最老任务策略。·CallerRunsPolicy:调用者执行策略。·自定义策略。

  • AbortPolicy使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。
  • DiscardPolicy该策略是AbortPolicy的Silent(安静)版本,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。
  • DiscardOldestPolicy抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。
  • CallerRunsPolicy调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。在以上4种内置策略中,线程池默认的拒绝策略为AbortPolicy,如果提交的任务被拒绝,线程池就会抛出RejectedExecutionException异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获RejectedExecutionExcep
  • 如果以上拒绝策略都不符合需求,那么可自定义一个拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可。

1.5.9 优雅的关闭线程

线程的5种状态:

     package java.util.concurrent;
     // 省略import
     public class ThreadPoolExecutor extends AbstractExecutorService {
          // runState is stored in the high-order bits
         private static final int RUNNING   = -1 << COUNT_BITS;
         private static final int SHUTDOWN          =  0 << COUNT_BITS;
         private static final int STOP              =  1 << COUNT_BITS;
         private static final int TIDYING           =  2 << COUNT_BITS;
         private static final int TERMINATED        =  3 << COUNT_BITS;
         // 省略其他
     }
  • RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
  • SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
  • STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
  • TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
  • TERMINATED:执行完terminated()钩子方法之后的状态。

关闭线程的方法

shutdown:是JUC提供的一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后,才会执行关闭,但是此方法被调用之后线程池的状态转为SHUTDOWN,线程池不会再接收新的任务。 shutdownNow:是JUC提供的一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。 awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法。

源码分析

shutdown

        public void shutdown()
         {
             final ReentrantLock mainLock = this.mainLock;
             mainLock.lock();
             try
             {
                 // 检查权限
                 checkShutdownAccess();
                 // 设置线程池状态
                 advanceRunState(SHUTDOWN);
                 // 中断空闲线程
                 interruptIdleWorkers();
                 // 钩子函数,主要用于清理一些资源
                 onShutdown();
             } finally
             {
                 mainLock.unlock();
             }
             tryTerminate();
         }
  • Shutdown()方法首先加锁,其次检查调用者是否用于执行线程池关闭的Java Security权限。
  • 接着shutdown()方法会将线程池状态变为SHUTDOWN,在这之后线程池不再接受提交的新任务。
  • 此时如果还继续往线程池提交任务,将会使用线程池拒绝策略响应,默认的拒绝策略将会使用ThreadPoolExecutor.AbortPolicy,接收新任务时会抛出RejectedExecutionException异常。

shutdownNow()

       public List<Runnable> shutdownNow()
       {
             List<Runnable> tasks;
             final ReentrantLock mainLock = this.mainLock;
             mainLock.lock();
             try
             {
                 // 检查状态
                 checkShutdownAccess();
                 // 将线程池状态变为 STOP
                 advanceRunState(STOP);
                 // 中断所有线程,包括工作线程以及空闲线程
                 interruptWorkers();
                 // 丢弃工作队列中的剩余任务
                 tasks = drainQueue();
             } finally
             {
                 mainLock.unlock();
             }
             tryTerminate();
             return tasks;
       }
  • shutdownNow()方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及空闲线程),最后清空工作队列,取出工作队列所有未完成的任务返回给调用者。
  • 与有序的shutdown()方法相比,shutdownNow()方法比较粗暴,直接中断工作线程。不过这里需要注意的是,中断线程并不代表线程立刻结束,只是通过工作线程的interrupt()实例方法设置了中断状态,这里需要用户程序主动配合线程进行中断操作。

awaitTermination()

     threadPool.shutdown();
     try {
         //一直等待,直到线程池完成关闭
         while (!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
             System.out.println("线程池任务还未执行结束");
         }
     } catch (InterruptedException e) {
         e.printStackTrace();
     }

如果线程池完成关闭,awaitTermination()方法将会返回true,否则当等待时间超过指定时间后将会返回false。如果需要调用awaitTermination(),建议不是永久等待,而是设置一定重试次数。

优雅的关闭

package Utils;
​
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
​
/**
 * @description:
 * @author: shu
 * @createDate: 2022/10/31 20:19
 * @version: 1.0
 */
public class ThreadUtils {
​
    
        public static void shutdownThreadPoolGracefully(
                ExecutorService threadPool)
        {
            // 若已经关闭则返回
            if (!(threadPool instanceof ExecutorService) ||
                    threadPool.isTerminated())
            {
                return;
            }
            try
            {
                threadPool.shutdown();   //拒绝接受新任务
            } catch (SecurityException e)
            {
                return;
            } catch (NullPointerException e)
            {
                return;
            }
            try
            {
                // 等待60秒,等待线程池中的任务完成执行
                if (!threadPool.awaitTermination(60, TimeUnit.SECONDS))
                {
                    // 调用 shutdownNow() 方法取消正在执行的任务
                    threadPool.shutdownNow();
                    // 再次等待60秒,如果还未结束,可以再次尝试,或者直接放弃
                    if (!threadPool.awaitTermination(60, TimeUnit.SECONDS))
                    {
                        System.err.println("线程池任务未正常执行结束");
                    }
                }
            } catch (InterruptedException ie)
            {
                // 捕获异常,重新调用 shutdownNow() 方法
                threadPool.shutdownNow();
            }
            // 仍然没有关闭,循环关闭1000次,每次等待10毫秒
            if (!threadPool.isTerminated())
            {
                try
                {
                    for (int i = 0; i < 1000; i++)
                    {
                        if (threadPool.awaitTermination(10, TimeUnit.MILLISECONDS))
                        {
                            break;
                        }
                        threadPool.shutdownNow();
                    }
                } catch (InterruptedException e)
                {
                    System.err.println(e.getMessage());
                } catch (Throwable e)
                {
                    System.err.println(e.getMessage());
                }
            }
        }
​
}
​