Java 多线程基础知识整理

多线程

多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。

概念

程序:静态的,是指令的有序集合

进程(process):动态,是程序的一次动态执行过程,是资源分配的基本单位,每个进程都有独立的 CPU 时间、code、data(即使是同一程序产生多个进程),导致浪费内存,开销较大(Ctrl+Alt+Del—启动任务管理器查看所有进程)

进程特征:

  • 独立性:系统中独立存在的实体,有独立的资源

  • 动态性:正在系统中活动的指令集合

  • 并发性:单个处理器上可执行多个进程

线程:进程的一个实体,CPU 调度的基本单位不能单独执行

进程拥有独立的地址空间(内存单元),线程没有(仅有独立的栈空间,但堆空间和方法区共享)。一个进程至少有一个线程,同一进程内多个线程共享其资源,提高效率;但是,多个线程共享的系统资源可能就会带来安全隐患。

并行多个 CPU 同时执行多个任务,多个人同时做不同的事

并发一个 CPU 同时执行多个任务,多个人同时做同一件事(如秒杀)

单核 CPU VS 多核 CPU:

单核 CPU 是一种假的多线程,因为在一个时间单元内只能执行一个线程的任务。不能实现真正的多并发,因为单核 CPU 一个时间点处理一个事件,仅仅是在多个线程之间频繁切换。只是因为 CPU 时间单元特别短,因此感觉不出来;多核 CPU (现在服务器都是多核)才能更好的发挥多线程的效率。

为什么要用多线程?

  1. 提高应用程序的响应性,对图形化界面更有意义,可增强用户体验;
  2. 提高计算机系统 CPU 的利用率,提高系统吞吐量;
  3. 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,易于理解和修改。
  4. 这些情况下通常要用:当程序需要同时执行两个或多个任务;需要实现一些需要等待的任务,如用户输入、文件读写操作、网络操作、搜索等;需要一些后台运行的程序等。

多线程难点

单线程只有一条执行线,过程容易理解,可以在大脑中清晰勾勒出代码的执行流程;多线程却是多条线,而且一般多条线之间有交互、需要通信。

  1. 多线程的执行结果不确定,受 CPU 调度的影响;
  2. 多线程的安全问题;
  3. 线程资源宝贵,依赖线程池操作线程时,线程池的参数设置问题;
  4. 多线程执行是动态的、同时的,难以追踪过程;
  5. 多线程的底层是操作系统层面的,源码难度大。

线程生命周期

IMG_0301_20220226-101604_.PNG

线程调度

抢占时间片:高优先级的线程有更大概率能抢占 CPU 的时间片,同优先级的线程组成队列(先到先服务)。

线程创建时继承父线程的优先级。

上下文切换

指处理器 CPU 从执行一个线程切换到另一个线程。

多核 CPU 下,多线程是并发工作的,如果线程数多,单个核又会并发调度线程,这样运行时就会有上下文切换的概念。CPU 执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

  1. 线程的 CPU 时间片用完
  2. 垃圾回收
  3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,JVM 中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,线程私有。

线程创建

  1. 继承 Thread 类,重写 run() 方法,将线程要执行的逻辑声明在 run() 中。

    启动线程:子类对象.start(); 启动线程,线程(由 JVM)再调用 run() 方法。

    缺点:OOP 单继承局限性(无法继承其他类,Java 仅支持接口多实现和类的单继承),每个任务的成员变量不共享,只能用 static 修饰,才能做到多线程共享。

    问题:多个线程共用一个资源的情况下(购票),线程不安全、数据紊乱。

  2. 实现 Runnable 接口,重写 run() 方法,将线程要执行的逻辑声明在 run() 中。

    启动线程:创建目标对象 + new Thread(目标对象).start(); //通过 Thread 类含参构造器创建线程对象,即将 Runnable 接口的实现类对象作为实际参数传递给 Thread 类的构造器中

    优点:可以继承其他类,避免单继承的局限性。

    问题:线程不安全,多个线程可以共享同一个接口实现类的对象。

  3. 实现 Callable 接口,重写 call() 方法(API 实现, JDK 1.5 之后)

    启动线程:与 Runnable 一样,创建目标对象 + new Thread(目标对象).start(); 即将 Callable 接口的实现类对象作为实际参数传递给 Thread 类的构造器中

    优点:可以抛出异常,支持泛型的返回值(可以获取线程执行结构),需要借助 TutureTask 类,获取返回结果。

    缺点:效率较低,get() 方法是在当前线程中获取其他线程的执行结果,可能导致当前线程阻塞

    补充:Future 接口,可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask 是其唯一实现类同时又实现了 Runnable 接口,它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

  4. 使用线程池:使用 JUC( java.util.concurrent )中的线程池创建( JDK 1.5 之后)

    对于经常创建和销毁、使用量特别大地资源,性能影响很大。

    思路:提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池子。避免频繁地创建销毁、实现重复利用。目的就是为了更好地支持高并发任务,让开发者进行多线程编程时减少竞争条件和死锁的问题。

    优点:提高响应速度(减少了创建新线程地时间)、降低资源消耗(重复利用线程池中的资源,不需要每次都创建、销毁)、便于线程管理(线程池可以对线程的创建、停止、数量等因素加以控制,使线程在一种可控的范围内)。

    corePoolSize:核心池的大小

    maximumPoolSize:最大线程数

    keepAliveTime:线程生命周期,没有任务时最多保持多久后终止

线程启动

创建线程时,thread.start() 方法在 JVM 中开辟一个新的栈空间供该分支线程使用。

每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作,run() 方法的主体称为线程体。

注意:启动线程是通过 start() ,不是直接调用 run() 。一个线程对象只能调用一次 start() 方法,如果重复调用,会抛出异常 IllegalThreadStateException。

线程常用方法

主要是 Thread 类中的核心方法:

void start():线程启动,进入就绪状态,等待 CPU 分配时间片,执行 run() 方法

void run():线程获取到 CPU 时间片时执行的具体逻辑

string Thread.currentThread().getName():获取线程对象名称

void setName(String name):设置线程对象名称

static Thread currentThread():获取当前线程对象

static void sleep(long mills):当前线程进入休眠(阻塞状态),不会释放锁

static void yield():礼让,放弃当前线程获取的 CPU 时间片,只是让线程从运行状态进入就绪状态,而不是阻塞状态,其他线程不一定就可以抢占到 CPU 时间片

void join():插队,等待该线程执行结束后再执行其他线程

boolean interrupt():用于中断线程

void stop():强行终止线程执行(不推荐使用,容易丢失数据)

int getPriority():获取线程优先级(默认5,最低1,最高10)

void setPriority(int newPriority):设置线程优先级

boolean isAlive():判断当前线程是否存活

主要是 Object 类中的相关方法:

wait():获取到锁的当前线程进入阻塞状态,会立即释放锁

notify():唤醒正在排队等待同步资源的最高优先级的线程。

notifyAll():唤醒正在排队等待同步资源的所有线程

用户线程和守护线程

唯一的区别就是判断 JVM 何时离开,若 JVM 中都是守护线程,当前 JVM 就会退出。

① 用户线程:虚拟机必须确保其执行完毕,如 main() —主线程

② 守护线程:虚拟机不用等待其执行完毕,守护线程内部是一个死循环,如 gc() 垃圾回收机制、异常处理机制、等待机制、监控机制

③ 正常线程都是用户线程,setDaemon(true) —设置该线程为守护线程

补充:定时器机制

间隔特定时间执行特点程序,可以用到 Thread.sleep() 方法,还有 java.util.Timer 类、Spring Task 框架。

作用:定时器和守护线程联合使用实现数据自动备份。

一个 Java 应用程序 java.exe,至少有三个线程:main() 主线程、gc() 垃圾回收线程、异常处理线程。

线程安全

概念

Java 采用的是抢占式调度模型:线程抢占的 CPU 时间片多少取决于优先级,还有一种是均匀式调度模型:平均分配线程抢占的 CPU 时间片。

多线程出现安全问题的原因:当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程就参与进来,导致共享数据的错误。

解决:对于有多条语句操作共享对象的情况,只能让一个线程执行完,在执行过程中其他线程不可以参与执行。

注意:常量(不可修改)和局部变量(存在栈中,而栈内存线程独有,变量不可能被共享)不存在线程安全问题。

三性

  1. 原子性 Atomic:不可分割,一个线程访问某个共享变量时,从其他线程来看,该操作要么已经执行完毕,要么尚未发生,看不到当前操作的中间结果。保证了线程安全,不会受到上下文切换的影响。

    Java 有两种方式实现原子性:锁(锁具有排他性)、CAS指令(硬件锁,直接在硬件层次上实现

  2. 可见性 Visibility:在多线程环境中,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。

  3. 有序性 Ordering:JVM 在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为指令重排序。

同步与异步编程模型

  1. 同步:线程之间存在等待关系,线程排队执行

    实现:

    • 共享对象用 synchronized 修饰:Java 中每个对象都有属于自己的内部锁标记 Monitor
    • 实例方法用 synchronized 修饰:该方法表示共享对象一定是 this,且同步的范围是整个方法体,不灵活,效率较低(无故扩大同步范围)
    • 静态方法用 synchronized 修饰:表示线程对象要寻找类的锁才能进入就绪状态
  2. 异步:线程之间无等待关系,多线程并发

    解决

    • 使用局部变量 + 静态变量代替成员变量
    • 一个线程对应一个对象,对象不共享
    • 在 Java 中通过线程同步机制:synchronized 来解决线程安全问题

同步机制

Java 为解决多线程安全问题提出的专业解决方式,可以将它理解为线程之间按照一定的顺序执行。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序不会影响单线程执行结果,但是会影响到多线程并发执行结果的正确性。

Java 平台提供的线程机制包括锁,volatile 关键字(Lock 通过它),final 关键字,static 关键字,以及相关的 API:Object.wait(); 、Object.notify() 等

锁的概念

一个线程只能在有锁的时候才能访问共享数据,访问结束后必须释放锁。

锁具有排他性,即一个锁只能被一个线程持有,这种锁被称为互斥锁。锁可以实现对共享数据的安全访问,保障线程的原子性、可见性、有序性。

持有锁和释放锁之间所执行的代码叫做临界区(CriticalSection)

JVM 把锁分为内部锁显示锁两种。内部锁通过 synchronized 关键字实现,显示锁通过 java.concurrent.locks.Lock 接口实现类实现。

内部锁 synchronized

Java 中的每一个对象都自动含有一个与之关联的内部锁(也叫对象锁、同步锁、监视器),是一种排他锁,所以任意对象都可以作为同步锁。

使用场景:

  1. 同步代码块:

    synchronized(对象锁 / 同步监视器){//需要被同步的代码块}

    作用的对象是调用这个代码块的对象,括号中的对象锁由自己指定,很多时候指定为 this 或 类名.class。

  2. 同步方法:

    synchronized 声明在方法中。

    作用的范围是整个方法作用的对象是调用这个方法的对象

    当 synchronized 修饰一个静态方法时,其作用的范围是整个静态方法,作用的对象是这个类的所有对象

注意:必须确保使用同一资源的多个线程共用一把锁。一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用一把锁(this)。

同步代码块比同步方法效率更高,线程出现异常时会自动释放锁。

同步的范围:明确哪些代码是多线程运行的,明确多个线程中是否有共享数据,明确多线程运行代码中是否有多条语句操作共享资源。对多条操作共享数据的语句块,我们只能让一个线程都执行完,过程中其他线程不能参与执行。也就是说,所有操作共享数据的语句都要放在同步范围内。不能太大(没发挥多线程功能)也不能太小(没锁住)。

释放锁的操作:

  1. 当前线程的同步方法、同步代码块执行结束
  2. 当前线程在同步方法、同步代码块中遇到 break、return 终止了代码块、方法的继续执行
  3. 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束
  4. 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停并释放锁(wait 会释放锁)。

不会释放锁的操作:

  1. 线程在执行同步方法、同步代码块时,程序调用了 Thread.sleep()Thread.yield(),暂停当前线程的执行。
  2. 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,不会释放锁(同步监视器),要尽量避免使用。
volatile

该关键字解决了并发编程中的可见性和有序性,但是无法保证原子性,只能修饰变量,开发中使用 synchronized 的比例较大。

synchronized 实现的锁本质上是一种阻塞锁,即多个线程要排队访问同一个共享对象,是一种锁机制,存在阻塞问题和性能问题;而 volatile 是 Java 虚拟机提供的一种轻量级同步机制,基于内存屏障实现,并不是锁,所以不会有 synchronized 带来的阻塞和性能损耗的问题。除了 volatile 比 synchronized 性能好以外,由于 volatile 借助了内存屏障来帮助其解决可见性和有序性问题,内存屏障的使用还为其带来了一个禁止指令重排的附件功能。

所以,在需要做并发控制的时候,如果不涉及到原子性的问题,可以优先考虑使用 volatile 关键字。

synchronized VS volatile
  1. volatile 只能用在变量上;synchronized 可以用在代码块和方法上;
  2. volatile 不是锁,而是一种线程同步的实现,不会出现线程阻塞,性能优于 synchronized ;synchronized 是同步锁机制,本质上是阻塞锁,可能会造成线程的阻塞;
  3. volatile 保证变量在多个线程之间的可见性,但不能保证原子性;synchronized 可以保证;
  4. volatile 本质上是告诉 JVM 当前变量在内存中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程阻塞。
Lock(锁)

JDK 1.5 开始,Java 提供了更加强大的而线程同步机制——通过显式定义同步锁的对象来实现同步。同步锁使用 Lock 对象充当。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,线程开始访问共享资源前应先获得 Lock 对象。

例:

class A{
    private final ReentrantLock lock=new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
            //保证线程安全的代码;
        }finally{
            lock.unlock();//如果同步代码块有异常,要将unlock()写入这里
        }
    }
}
synchronized VS Lock
  1. Lock 是接口;synchronized 是关键字
  2. Lock 是显式锁,显示地指定开始与结束的位置,即手动开启和关闭锁;synchronized 是隐式锁,离开作用域(同步代码块、同步方法)或线程抛出异常时,就会自动释放锁;
  3. Lock 锁只有代码块锁;synchronized 有代码块锁和方法锁;
  4. Lock 可以让等待锁的线程响应中断,而 synchronized 会让等待的线程一直等下去,不能够响应中断;
  5. Lock 可以知道是否已经获得锁;synchronized 无法知道;
  6. 从性能上,若资源竞争不激烈,两者性能相差不大;若资源竞争相当激烈,此时 Lock 的性能远优于 synchronized。

锁的类型

可重入锁

可重入就是说,当一个线程执行到某个 synchronized 方法 method1 ,而 method1 中又调用了另一个 synchronized 方法 method2 时,该线程不需要重新去申请锁,而是直接执行 method2 。

synchronized 和 ReentrantLock(Lock) 都是可重入锁,具备可重入性。

可中断锁

可中断就是说,若线程 A 正在执行锁中的代码,线程 B 在等待获取该锁,由于等待时间过长,线程 B 不想继续等待转而处理其他事情,那么我们可以让它中断自己或在别的线程中中断它。

Lock 是可中断锁,可以响应中断;synchronized 不是。

公平锁

公平锁就是说,尽量以线程请求锁的顺序来获取锁,非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样就会导致某个或者一些线程可能永远也获取不到锁。

synchronized 是非公平锁,无法保证等待的线程依次按顺序获取锁;ReentrantLock 和 ReentrantWriteLock 默认情况是非公平锁,但是可以设置为公平锁:ReentrantLock lock = new ReentrantLock(true);

死锁

指不同的线程因争夺系统资源而产生相互等待的现象,即两个线程都在等待对方释放同步监视器。一旦出现死锁,整个程序不会发生异常,也不会有任何提示。

形成死锁的条件

  1. 互斥:某资源只允许一个线程访问
  2. 占有且等待:资源请求者在请求其他的资源的同时,保持对原有资源的占有
  3. 不可抢占:已经被分配的资源不能被其他线程抢夺,只能由资源占有者主动释放
  4. 循环等待:若干进程组成环路,环路中每个进程都在等待相邻进程释放资源

避免死锁

  1. 破坏条件2,所有进程在开始运行前,必须一次性申请需要的全部资源,但是降低了资源利用率
  2. 减少同步共享变量
  3. 多线程之间规定先后执行顺序
  4. 减少锁的嵌套

线程通信

以下三个方法都定义在 Object 类中,因为它们都需要用到锁,而任意对象都可以充当锁,所以它们定义在所有对象的超级父类 Object 中。

所以它们需要使用在有锁的地方,也就是需要用 synchronize 关键字来标识的区域,即使用在同步代码块或同步方法中,为了保证 wait 和 notify 的区域是同一个锁住的区域锁需要相同的对象来充当

wait():令当前线程挂起并放弃 CPU 和同步资源(释放锁)并等待,使别的线程可以访问并修改共享资源,而当前线程排队,等待其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获取对监视器的所有权后才能执行。

notify():唤醒正在排队等待同步资源的最高优先级的线程。

notifyAll():唤醒正在排队等待同步资源的所有线程

生产者消费者模型

以下三个方法只有在 synchronized 方法或代码块中,才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。因为这三个方法必须有锁对象调用对象.方法),而任意对象都可以作为 synchronized 的同步锁。

原理:

  1. 生产者和消费者各自有一个请求队列(内存缓冲区),”仓库“,可以使用 List
  2. 内存缓冲区为空时,消费者线程必须等待
  3. 内存缓冲区为满时,生产者线程必须等待
  4. 不空也不满时,两者之间既可以是动态平衡,也可以非动态平衡

相关方法:

  1. obj.wait():使对象 obj 上的活动线程进入无限等待状态,并且释放占有锁
  2. obj.notify():唤醒对象 obj 上的任一等待线程
  3. obj.notifyAll():唤醒对象 obj 上的所有等待线程

线程池

概述

经常创建和销毁使用量大的资源,比如并发情况下的线程,对系统性能的影响较大。所以提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中,以此避免频繁创建销毁,实现重复利用(如 ORM 工具的数据库连接池)。

线程池目的在于控制运行的线程数量,处理过程中将任务放到队列,然后在线程创建后,启动这些任务,如果线程数量超出了最大数量就排队等候,等其他线程执行完毕再从队列中取出任务执行。

优点

  1. 减少资源的消耗,通过池化地思想,即减少每次创建线程、销毁线程的开销;
  2. 提高相应速度,每次请求到来时,由于线程的创建已经完成,所以可以直接执行任务
  3. 提高线程的可管理性、扩展性,线程池可以对线程的创建与停止、线程数量等因素加以限制,使得线程在一种可控的范围内运行,方便性能调优。

线程池的状态

  1. Running:正常接收任务、处理任务
  2. Shutdown:不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
  3. Stop:不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
  4. Tidying:任务全部执行完毕,当前活动线程是0,即将进入终结
  5. Termitted:终结状态

处理流程

20220318185900.png

  1. 创建线程池后,线程池的状态是 Running,该状态下才能有下面的步骤,注意在刚创建线程池时,里面是没有线程的,任务队列作为参数传进来,要调用 execute() 方法;
  2. 提交任务时,线程池会创建线程去处理任务;
  3. 当线程池的工作线程数达到 corePoolSize 时,继续提交任务会进入阻塞队列;
  4. 当阻塞队列装满时,继续提交任务,会创建救急线程来处理;
  5. 当线程池中的工作线程数达到 maximumPoolSize 时,会执行拒绝策略
  6. 当线程取任务的时间达到 keepAliveTime 还没有取到任务,且工作线程数大于 corePoolSize 时,会回收该线程;
  7. 关闭线程池:shutdown(),shutdownNow()。

拒绝策略

  1. 调用者抛出 RejectedExecutionException (默认策略)
  2. 让调用者运行任务
  3. 丢弃此次任务
  4. 丢弃阻塞队列中最早的任务,而加入本任务

线程池相关 API

Executor

Java 中线程池是通过 Executor 框架实现的,它是所有线程池的接口,其中只有一个方法,void execute(Runnable command)。

void execute(Runnable command):执行命令,无返回值,一般用来执行 Runnable

<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable

void.shutdown():关闭连接池

Executor 两层调度模型

20220318180443.png

在 HotSpot 虚拟机中,Java 中的线程将会被一一映射为操作系统的线程。在 Java 虚拟机层面,用户将多个任务提交给 Executor 框架,Executor 负责分配线程执行;在操作系统层面,操作系统再将这些线程分配给处理器执行。

Executor 结构

20220318181154.png

  1. 任务:两种类型 Runnable 、Callable
  2. 任务执行器:Executor,框架最核心的接口。子接口是 ExecutorService,ExecutorService 有两大实现类 ThreadPoolExecutor 、ScheduledThreadExecutor。
  3. 执行结果:Future 接口,表示异步的执行的结果,实现类是 FutureTask。
Executors

工具类、线程池的工厂类,提供了一系列工厂方法,用于创建并返回不同类型的线程池,返回的线程池都实现了 ExecutorService 接口

四种类型的线程池:

  1. FixedThreadPool 定长线程池

    创建:Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池,是一种固定大小的线程池

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

    corePoolSize 和 maximumPoolSize 都为用户设定的线程数量 nThreads;

    keepAliveTime 为 0 意味着一旦有多余的空闲线程,该线程就会被立即停止掉;

    但是上面写的无效,原因是阻塞队列用了 LinkedBlockingQueue,这是一个无界队列,永远不可能拒绝任务

    实际线程将永远维持在 nThreads,所以这时的 maximumPoolSize 和 keepAliveTime 的设置都将无效。

  2. CachedThreadPool 可缓存线程池

    创建:Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

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

    corePoolSize 为 0,maximumPoolSize 为无限大,意味着线程数量可以无限大,适合处理执行时间较小的任务;

    keepAliveTime 为 60s 意味着线程空闲时间超过 60s 就会被杀死;

    采用 SynchronousQueue 装载等待的任务,这个阻塞队列没有存储空间,意味着只要有请求到来,就必须找到一条工作线程处理,如果当前没有空闲的线程,那么就会创建一条新的线程。

  3. SingleThreadExecutor 单一线程池

    创建:Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池

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

    只会创建一条工作线程处理任务,采用的阻塞队列为 LinkedBlockingQueue。

  4. ScheduledThreadPool 可调度的线程池

    创建:Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或定期执行,用于处理延时任务或定时任务。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    //ScheduledThreadPoolExecutor():
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
    

    它接收 ScheduledFutureTask 类型的内容,有两种提交方式:

    1. scheduledAtFixedRate
    2. scheduledWithFixedDelay

    它采用 DelayQueue 存储等待的任务,DelayQueue 是一个无界队列,内部封装了一个 PriorityQueue,它会根据 time 的先后顺序排序,若 time 相同则会根据 sequenceNumber 排序。

ExecutorService

Executor 的子接口,增加了 Executor 的行为,同时也是真正的线程池接口,是 Executor 实现类的最直接接口。

ThreadPoolExecutor

实现了 ExecutorService 接口,线程池的具体实现类,即线程池的真正实现,一般用的各种线程池都是基于这个类实现的,通过构造方法的一系列参数,来构成不同配置的线程池。

//ThreadPoolExecutor的构造方法
public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSiza,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler){
   
}
参数介绍
  1. corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过该数
  2. maximumPoolSiza:线程池允许的最大线程数(核心线程 + 非核心线程)
  3. keepAliveTime:线程池中线程闲置超时时长
  4. unit:表示 keepAliveTime 的单位
  5. workQueue:线程池中的任务队列,维护着等待执行的 Runnable 对象(线程)。即存放任务的 BlockingQueue<Runnable> 队列。
  6. threadFactory:创建线程的工厂,主要定义线程名
  7. handler:拒绝策略
补充:BlockingQueue

BlockingQueue 是阻塞队列,是 java.util.concurrent 下的主要用来控制线程同步的工具。如果 BlockingQueue 为空,则从 BlockingQueue 中取东西的操作将会被阻断,进入等待状态,直到 BlockingQueue 里进了东西才会被唤醒。同样,如果 BlockingQueue 为满,则往 BlockingQueue 里存东西的操作会被阻断。阻塞队列常常用于生产者消费者模型,其具体的实现类有 LinkedBlockingQueue,ArrayBlockingQueue 等,一般其内部都是通过显示锁 Lock 来实现阻塞和唤醒。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值