多线程
多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发状态的实现。
概念
程序:静态的,是指令的有序集合
进程(process):动态,是程序的一次动态执行过程,是资源分配的基本单位,每个进程都有独立的 CPU 时间、code、data(即使是同一程序产生多个进程),导致浪费内存,开销较大(Ctrl+Alt+Del—启动任务管理器查看所有进程)
进程特征:
-
独立性:系统中独立存在的实体,有独立的资源
-
动态性:正在系统中活动的指令集合
-
并发性:单个处理器上可执行多个进程
线程:进程的一个实体,CPU 调度的基本单位,不能单独执行
进程拥有独立的地址空间(内存单元),线程没有(仅有独立的栈空间,但堆空间和方法区共享)。一个进程至少有一个线程,同一进程内多个线程共享其资源,提高效率;但是,多个线程共享的系统资源可能就会带来安全隐患。
并行:多个 CPU 同时执行多个任务,多个人同时做不同的事
并发:一个 CPU 同时执行多个任务,多个人同时做同一件事(如秒杀)
单核 CPU VS 多核 CPU:
单核 CPU 是一种假的多线程,因为在一个时间单元内只能执行一个线程的任务。不能实现真正的多并发,因为单核 CPU 一个时间点处理一个事件,仅仅是在多个线程之间频繁切换。只是因为 CPU 时间单元特别短,因此感觉不出来;多核 CPU (现在服务器都是多核)才能更好的发挥多线程的效率。
为什么要用多线程?
- 提高应用程序的响应性,对图形化界面更有意义,可增强用户体验;
- 提高计算机系统 CPU 的利用率,提高系统吞吐量;
- 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,易于理解和修改。
- 这些情况下通常要用:当程序需要同时执行两个或多个任务;需要实现一些需要等待的任务,如用户输入、文件读写操作、网络操作、搜索等;需要一些后台运行的程序等。
多线程难点
单线程只有一条执行线,过程容易理解,可以在大脑中清晰勾勒出代码的执行流程;多线程却是多条线,而且一般多条线之间有交互、需要通信。
- 多线程的执行结果不确定,受 CPU 调度的影响;
- 多线程的安全问题;
- 线程资源宝贵,依赖线程池操作线程时,线程池的参数设置问题;
- 多线程执行是动态的、同时的,难以追踪过程;
- 多线程的底层是操作系统层面的,源码难度大。
线程生命周期
线程调度
抢占时间片:高优先级的线程有更大概率能抢占 CPU 的时间片,同优先级的线程组成队列(先到先服务)。
线程创建时继承父线程的优先级。
上下文切换
指处理器 CPU 从执行一个线程切换到另一个线程。
多核 CPU 下,多线程是并发工作的,如果线程数多,单个核又会并发调度线程,这样运行时就会有上下文切换的概念。CPU 执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。
- 线程的 CPU 时间片用完
- 垃圾回收
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,JVM 中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,线程私有。
线程创建
-
继承 Thread 类,重写 run() 方法,将线程要执行的逻辑声明在 run() 中。
启动线程:子类对象.start(); 启动线程,线程(由 JVM)再调用 run() 方法。
缺点:OOP 单继承局限性(无法继承其他类,Java 仅支持接口多实现和类的单继承),每个任务的成员变量不共享,只能用 static 修饰,才能做到多线程共享。
问题:多个线程共用一个资源的情况下(购票),线程不安全、数据紊乱。
-
实现 Runnable 接口,重写 run() 方法,将线程要执行的逻辑声明在 run() 中。
启动线程:创建目标对象 + new Thread(目标对象).start(); //通过 Thread 类含参构造器创建线程对象,即将 Runnable 接口的实现类对象作为实际参数传递给 Thread 类的构造器中。
优点:可以继承其他类,避免单继承的局限性。
问题:线程不安全,多个线程可以共享同一个接口实现类的对象。
-
实现 Callable 接口,重写 call() 方法(API 实现, JDK 1.5 之后)
启动线程:与 Runnable 一样,创建目标对象 + new Thread(目标对象).start(); 即将 Callable 接口的实现类对象作为实际参数传递给 Thread 类的构造器中。
优点:可以抛出异常,支持泛型的返回值(可以获取线程执行结构),需要借助 TutureTask 类,获取返回结果。
缺点:效率较低,get() 方法是在当前线程中获取其他线程的执行结果,可能导致当前线程阻塞。
补充:Future 接口,可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。FutureTask 是其唯一实现类,同时又实现了 Runnable 接口,它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
-
使用线程池:使用 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 时间片。
多线程出现安全问题的原因:当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程就参与进来,导致共享数据的错误。
解决:对于有多条语句操作共享对象的情况,只能让一个线程执行完,在执行过程中其他线程不可以参与执行。
注意:常量(不可修改)和局部变量(存在栈中,而栈内存线程独有,变量不可能被共享)不存在线程安全问题。
三性
-
原子性 Atomic:不可分割,一个线程访问某个共享变量时,从其他线程来看,该操作要么已经执行完毕,要么尚未发生,看不到当前操作的中间结果。保证了线程安全,不会受到上下文切换的影响。
Java 有两种方式实现原子性:锁(锁具有排他性)、CAS指令(硬件锁,直接在硬件层次上实现)
-
可见性 Visibility:在多线程环境中,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
-
有序性 Ordering:JVM 在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为指令重排序。
同步与异步编程模型
-
同步:线程之间存在等待关系,线程排队执行
实现:
- 共享对象用 synchronized 修饰:Java 中每个对象都有属于自己的内部锁标记 Monitor
- 实例方法用 synchronized 修饰:该方法表示共享对象一定是 this,且同步的范围是整个方法体,不灵活,效率较低(无故扩大同步范围)
- 静态方法用 synchronized 修饰:表示线程对象要寻找类的锁才能进入就绪状态
-
异步:线程之间无等待关系,多线程并发
解决:
- 使用局部变量 + 静态变量代替成员变量
- 一个线程对应一个对象,对象不共享
- 在 Java 中通过线程同步机制:synchronized 来解决线程安全问题
同步机制
Java 为解决多线程安全问题提出的专业解决方式,可以将它理解为线程之间按照一定的顺序执行。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序不会影响单线程执行结果,但是会影响到多线程并发执行结果的正确性。
Java 平台提供的线程机制包括锁,volatile 关键字(Lock 通过它),final 关键字,static 关键字,以及相关的 API:Object.wait(); 、Object.notify() 等。
锁的概念
一个线程只能在有锁的时候才能访问共享数据,访问结束后必须释放锁。
锁具有排他性,即一个锁只能被一个线程持有,这种锁被称为互斥锁。锁可以实现对共享数据的安全访问,保障线程的原子性、可见性、有序性。
持有锁和释放锁之间所执行的代码叫做临界区(CriticalSection)
JVM 把锁分为内部锁、显示锁两种。内部锁通过 synchronized 关键字实现,显示锁通过 java.concurrent.locks.Lock 接口实现类实现。
内部锁 synchronized
Java 中的每一个对象都自动含有一个与之关联的内部锁(也叫对象锁、同步锁、监视器),是一种排他锁,所以任意对象都可以作为同步锁。
使用场景:
-
同步代码块:
synchronized(对象锁 / 同步监视器){//需要被同步的代码块}
作用的对象是调用这个代码块的对象,括号中的对象锁由自己指定,很多时候指定为 this 或 类名.class。
-
同步方法:
synchronized 声明在方法中。
作用的范围是整个方法,作用的对象是调用这个方法的对象。
当 synchronized 修饰一个静态方法时,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
注意:必须确保使用同一资源的多个线程共用一把锁。一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用一把锁(this)。
同步代码块比同步方法效率更高,线程出现异常时会自动释放锁。
同步的范围:明确哪些代码是多线程运行的,明确多个线程中是否有共享数据,明确多线程运行代码中是否有多条语句操作共享资源。对多条操作共享数据的语句块,我们只能让一个线程都执行完,过程中其他线程不能参与执行。也就是说,所有操作共享数据的语句都要放在同步范围内。不能太大(没发挥多线程功能)也不能太小(没锁住)。
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束
- 当前线程在同步方法、同步代码块中遇到 break、return 终止了代码块、方法的继续执行
- 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception,导致异常结束
- 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停并释放锁(wait 会释放锁)。
不会释放锁的操作:
- 线程在执行同步方法、同步代码块时,程序调用了 Thread.sleep() 、Thread.yield(),暂停当前线程的执行。
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,不会释放锁(同步监视器),要尽量避免使用。
volatile
该关键字解决了并发编程中的可见性和有序性,但是无法保证原子性,只能修饰变量,开发中使用 synchronized 的比例较大。
synchronized 实现的锁本质上是一种阻塞锁,即多个线程要排队访问同一个共享对象,是一种锁机制,存在阻塞问题和性能问题;而 volatile 是 Java 虚拟机提供的一种轻量级同步机制,基于内存屏障实现,并不是锁,所以不会有 synchronized 带来的阻塞和性能损耗的问题。除了 volatile 比 synchronized 性能好以外,由于 volatile 借助了内存屏障来帮助其解决可见性和有序性问题,内存屏障的使用还为其带来了一个禁止指令重排的附件功能。
所以,在需要做并发控制的时候,如果不涉及到原子性的问题,可以优先考虑使用 volatile 关键字。
synchronized VS volatile
- volatile 只能用在变量上;synchronized 可以用在代码块和方法上;
- volatile 不是锁,而是一种线程同步的实现,不会出现线程阻塞,性能优于 synchronized ;synchronized 是同步锁机制,本质上是阻塞锁,可能会造成线程的阻塞;
- volatile 保证变量在多个线程之间的可见性,但不能保证原子性;synchronized 可以保证;
- 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
- Lock 是接口;synchronized 是关键字;
- Lock 是显式锁,显示地指定开始与结束的位置,即手动开启和关闭锁;synchronized 是隐式锁,离开作用域(同步代码块、同步方法)或线程抛出异常时,就会自动释放锁;
- Lock 锁只有代码块锁;synchronized 有代码块锁和方法锁;
- Lock 可以让等待锁的线程响应中断,而 synchronized 会让等待的线程一直等下去,不能够响应中断;
- Lock 可以知道是否已经获得锁;synchronized 无法知道;
- 从性能上,若资源竞争不激烈,两者性能相差不大;若资源竞争相当激烈,此时 Lock 的性能远优于 synchronized。
锁的类型
可重入锁
可重入就是说,当一个线程执行到某个 synchronized 方法 method1 ,而 method1 中又调用了另一个 synchronized 方法 method2 时,该线程不需要重新去申请锁,而是直接执行 method2 。
synchronized 和 ReentrantLock(Lock) 都是可重入锁,具备可重入性。
可中断锁
可中断就是说,若线程 A 正在执行锁中的代码,线程 B 在等待获取该锁,由于等待时间过长,线程 B 不想继续等待转而处理其他事情,那么我们可以让它中断自己或在别的线程中中断它。
Lock 是可中断锁,可以响应中断;synchronized 不是。
公平锁
公平锁就是说,尽量以线程请求锁的顺序来获取锁,非公平锁无法保证锁的获取是按照请求锁的顺序进行的,这样就会导致某个或者一些线程可能永远也获取不到锁。
synchronized 是非公平锁,无法保证等待的线程依次按顺序获取锁;ReentrantLock 和 ReentrantWriteLock 默认情况是非公平锁,但是可以设置为公平锁:ReentrantLock lock = new ReentrantLock(true);
死锁
指不同的线程因争夺系统资源而产生相互等待的现象,即两个线程都在等待对方释放同步监视器。一旦出现死锁,整个程序不会发生异常,也不会有任何提示。
形成死锁的条件:
- 互斥:某资源只允许一个线程访问
- 占有且等待:资源请求者在请求其他的资源的同时,保持对原有资源的占有
- 不可抢占:已经被分配的资源不能被其他线程抢夺,只能由资源占有者主动释放
- 循环等待:若干进程组成环路,环路中每个进程都在等待相邻进程释放资源
避免死锁:
- 破坏条件2,所有进程在开始运行前,必须一次性申请需要的全部资源,但是降低了资源利用率
- 减少同步共享变量
- 多线程之间规定先后执行顺序
- 减少锁的嵌套
线程通信
以下三个方法都定义在 Object 类中,因为它们都需要用到锁,而任意对象都可以充当锁,所以它们定义在所有对象的超级父类 Object 中。
所以它们需要使用在有锁的地方,也就是需要用 synchronize 关键字来标识的区域,即使用在同步代码块或同步方法中,为了保证 wait 和 notify 的区域是同一个锁住的区域,锁需要相同的对象来充当。
wait():令当前线程挂起并放弃 CPU 和同步资源(释放锁)并等待,使别的线程可以访问并修改共享资源,而当前线程排队,等待其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获取对监视器的所有权后才能执行。
notify():唤醒正在排队等待同步资源的最高优先级的线程。
notifyAll():唤醒正在排队等待同步资源的所有线程。
生产者消费者模型
以下三个方法只有在 synchronized 方法或代码块中,才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。因为这三个方法必须有锁对象调用(对象.方法),而任意对象都可以作为 synchronized 的同步锁。
原理:
- 生产者和消费者各自有一个请求队列(内存缓冲区),”仓库“,可以使用 List ;
- 内存缓冲区为空时,消费者线程必须等待
- 内存缓冲区为满时,生产者线程必须等待
- 不空也不满时,两者之间既可以是动态平衡,也可以非动态平衡
相关方法:
- obj.wait():使对象 obj 上的活动线程进入无限等待状态,并且释放占有锁
- obj.notify():唤醒对象 obj 上的任一等待线程
- obj.notifyAll():唤醒对象 obj 上的所有等待线程
线程池
概述
经常创建和销毁使用量大的资源,比如并发情况下的线程,对系统性能的影响较大。所以提前创建好多个线程,放入线程池,使用时直接获取,使用完放回池中,以此避免频繁创建销毁,实现重复利用(如 ORM 工具的数据库连接池)。
线程池目的在于控制运行的线程数量,处理过程中将任务放到队列,然后在线程创建后,启动这些任务,如果线程数量超出了最大数量就排队等候,等其他线程执行完毕再从队列中取出任务执行。
优点
- 减少资源的消耗,通过池化地思想,即减少每次创建线程、销毁线程的开销;
- 提高相应速度,每次请求到来时,由于线程的创建已经完成,所以可以直接执行任务;
- 提高线程的可管理性、扩展性,线程池可以对线程的创建与停止、线程数量等因素加以限制,使得线程在一种可控的范围内运行,方便性能调优。
线程池的状态
- Running:正常接收任务、处理任务
- Shutdown:不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
- Stop:不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
- Tidying:任务全部执行完毕,当前活动线程是0,即将进入终结
- Termitted:终结状态
处理流程
- 创建线程池后,线程池的状态是 Running,该状态下才能有下面的步骤,注意在刚创建线程池时,里面是没有线程的,任务队列作为参数传进来,要调用 execute() 方法;
- 提交任务时,线程池会创建线程去处理任务;
- 当线程池的工作线程数达到 corePoolSize 时,继续提交任务会进入阻塞队列;
- 当阻塞队列装满时,继续提交任务,会创建救急线程来处理;
- 当线程池中的工作线程数达到 maximumPoolSize 时,会执行拒绝策略;
- 当线程取任务的时间达到 keepAliveTime 还没有取到任务,且工作线程数大于 corePoolSize 时,会回收该线程;
- 关闭线程池:shutdown(),shutdownNow()。
拒绝策略:
- 调用者抛出 RejectedExecutionException (默认策略)
- 让调用者运行任务
- 丢弃此次任务
- 丢弃阻塞队列中最早的任务,而加入本任务
线程池相关 API
Executor
Java 中线程池是通过 Executor 框架实现的,它是所有线程池的接口,其中只有一个方法,void execute(Runnable command)。
void execute(Runnable command):执行命令,无返回值,一般用来执行 Runnable
<T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable
void.shutdown():关闭连接池
Executor 两层调度模型
在 HotSpot 虚拟机中,Java 中的线程将会被一一映射为操作系统的线程。在 Java 虚拟机层面,用户将多个任务提交给 Executor 框架,Executor 负责分配线程执行;在操作系统层面,操作系统再将这些线程分配给处理器执行。
Executor 结构
- 任务:两种类型 Runnable 、Callable
- 任务执行器:Executor,框架最核心的接口。子接口是 ExecutorService,ExecutorService 有两大实现类 ThreadPoolExecutor 、ScheduledThreadExecutor。
- 执行结果:Future 接口,表示异步的执行的结果,实现类是 FutureTask。
Executors
工具类、线程池的工厂类,提供了一系列工厂方法,用于创建并返回不同类型的线程池,返回的线程池都实现了 ExecutorService 接口。
四种类型的线程池:
-
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 的设置都将无效。
-
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 装载等待的任务,这个阻塞队列没有存储空间,意味着只要有请求到来,就必须找到一条工作线程处理,如果当前没有空闲的线程,那么就会创建一条新的线程。
-
SingleThreadExecutor 单一线程池
创建:Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
public static ExecutorService newSingleThreadExecutor(){ return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }
只会创建一条工作线程处理任务,采用的阻塞队列为 LinkedBlockingQueue。
-
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 类型的内容,有两种提交方式:
- scheduledAtFixedRate
- 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){
}
参数介绍
- corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过该数
- maximumPoolSiza:线程池允许的最大线程数(核心线程 + 非核心线程)
- keepAliveTime:线程池中线程闲置超时时长
- unit:表示 keepAliveTime 的单位
- workQueue:线程池中的任务队列,维护着等待执行的 Runnable 对象(线程)。即存放任务的 BlockingQueue<Runnable> 队列。
- threadFactory:创建线程的工厂,主要定义线程名
- handler:拒绝策略
补充:BlockingQueue
BlockingQueue 是阻塞队列,是 java.util.concurrent 下的主要用来控制线程同步的工具。如果 BlockingQueue 为空,则从 BlockingQueue 中取东西的操作将会被阻断,进入等待状态,直到 BlockingQueue 里进了东西才会被唤醒。同样,如果 BlockingQueue 为满,则往 BlockingQueue 里存东西的操作会被阻断。阻塞队列常常用于生产者消费者模型,其具体的实现类有 LinkedBlockingQueue,ArrayBlockingQueue 等,一般其内部都是通过显示锁 Lock 来实现阻塞和唤醒。