多线程
# 多线程
# Java多线程概述
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
# 什么是线程?
- 线程(thread)是一个程序内部的一条执行路径。
- 我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
- 程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
# 多线程是什么?
多线程是指从软硬件上实现多条执行流程的技术。
再例如:消息通信、淘宝、京东系统都离不开多线程技术。
# 一个线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
# 多线程的创建
# 继承Thread类
Thread类
- Java是通过java.lang.Thread 类来代表线程的。
- 按照面向对象的思想,Thread类应该提供了实现多线程的方式。
多线程的实现方案一:继承Thread类
①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
②创建MyThread类的对象
③调用线程对象的start()方法启动线程(启动后还是执行run方法的)
package com.itheima.d2_demo1;
public class ThreadDemo1 {
public static void main(String[] args) {
// new一个新线程对象
Thread thread = new MyThread();
// 调用start方法启动线程,(执行的还是run方法)
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/*
1.定义一个线程类继承Thread类
*/
class MyThread extends Thread{
// 重写run方法,里面是定义线程以后要干啥
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
运行结果:
主线程执行输出:0
子线程执行输出:0
子线程执行输出:1
子线程执行输出:2
子线程执行输出:3
子线程执行输出:4
主线程执行输出:1
主线程执行输出:2
主线程执行输出:3
主线程执行输出:4
优缺点:
- 优点:编码简单
- 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。
总结:
1、为什么不直接调用了run方法,而是调用start启动线程。
- 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
- 只有调用start方法才是启动一个新的线程执行。
2、把主线程任务放在子线程之前了。
- 这样主线程一直是先跑完的,相当于是一个单线程的效果了。
# 实现Runnable接口
多线程的实现方案二:实现Runnable接口
①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
②创建MyRunnable任务对象
③把MyRunnable任务对象交给Thread处理。
④调用线程对象的start()方法启动线程
Thread的构造器
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
package com.itheima.d2_demo1;
public class ThreadDemo2 {
public static void main(String[] args) {
// 创建一个任务对象
Runnable target = new MyRunnable();
// 把任务对象交给Thread处理
Thread t = new Thread(target);
// 启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/*
1.定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable{
// 重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
运行结果:
主线程执行输出:0
主线程执行输出:1
主线程执行输出:2
主线程执行输出:3
主线程执行输出:4
主线程执行输出:5
主线程执行输出:6
主线程执行输出:7
主线程执行输出:8
主线程执行输出:9
子线程执行输出:0
子线程执行输出:1
子线程执行输出:2
子线程执行输出:3
子线程执行输出:4
优缺点:
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
- 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。
总结:
1.第二种方式是如何创建线程的?
- 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
- 创建MyRunnable对象
- 把MyRunnable任务对象交给Thread线程对象处理。
- 调用线程对象的start()方法启动线程
2.第二种方式的优点
- 优点:线程任务类只是实现了Runnale接口,可以继续继承和实现。
- 缺点:如果线程有执行结果是不能直接返回的。
实现Runnable接口(匿名内部类形式)
①可以创建Runnable的匿名内部类对象。
②交给Thread处理。
③调用线程对象的start()启动线程。
package com.itheima.d2_demo1;
public class ThreadDemo2Other {
public static void main(String[] args) {
// 第一种简写方法
Runnable target = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
};
Thread t = new Thread(target);
t.start();
// 第二种简洁方法
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("第二个子线程启动:" + i);
}
}
}).start();
// 第三种简写方法 使用Lamba函数
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("第二个子线程启动:" + i);
}
}).start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
运行结果:
子线程执行输出:0
子线程执行输出:1
子线程执行输出:2
子线程执行输出:3
子线程执行输出:4
第二个子线程启动:0
第二个子线程启动:1
第二个子线程启动:2
主线程执行输出:0
主线程执行输出:1
主线程执行输出:2
主线程执行输出:3
主线程执行输出:4
主线程执行输出:5
主线程执行输出:6
主线程执行输出:7
主线程执行输出:8
主线程执行输出:9
第二个子线程启动:0
第二个子线程启动:1
第二个子线程启动:2
# 实现Callable接口
需要思考的两个问题:
1、前2种线程创建方式都存在一个问题:
- 他们重写的run方法均不能直接返回结果。
- 不适合需要返回线程执行结果的业务场景。
2、怎么解决这个问题呢?
- JDK 5.0提供了Callable和FutureTask来实现。
这种方式的优点是:可以得到线程执行的结果。
多线程的实现方案三:利用Callable、FutureTask接口实现。
①、得到任务对象
1.定义类实现Callable接口,重写call方法,封装要做的事情。
2.用FutureTask把Callable对象封装成线程任务对象。
②、把线程任务对象交给Thread处理。
③、调用Thread的start方法启动线程,执行任务
④、线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。
FutureTask的API
方法名称 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |
public V get() throws Exception | 获取线程执行call方法返回的结果。 |
package com.itheima.d2_demo1;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadDemo3 {
public static void main(String[] args) {
// 创建Callable任务对象
Callable<String> call = new MyCallable(100);
// 把Callable任务对象 交给FutureTask对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口) 可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
Thread t1 = new Thread(f1);
t1.start();
Callable<String> call1 = new MyCallable(200);
FutureTask<String> f2 = new FutureTask<>(call1);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果
String rs1 = f1.get();
System.out.println("第一个子线程的结果" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果
try {
String rs2 = f2.get();
System.out.println("第二个子线程的结果" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
运行结果:
第一个子线程的结果子线程执行的结果是:4950
第二个子线程的结果子线程执行的结果是:19900
方式三优缺点
- 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
可以在线程执行完毕后去获取线程执行的结果。
- 缺点:编码复杂一点。
# Thread的常用方法
Thread获取和设置线程名称
方法名称 | 说明 |
---|---|
String getName() | 获取当前线程的名称,默认线程名称是Thread-索引 |
void setName(String name) | 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称 |
Thread类获得当前线程的对象
方法名称 | 说明 |
---|---|
public static Thread currentThread(): | 返回对当前正在执行的线程对象的引用 |
注意
1.此方法是Thread类的静态方法,可以直接使用Thread类调用。
2.这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。
Thread的构造器
方法名称 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target ,String name ) | 封装Runnable对象成为线程对象,并指定线程名称 |
Thread类的线程休眠方法
方法名称 | 说明 |
---|---|
public static void sleep(long time) | 让当前线程休眠指定的时间后再继续执行,单位为毫秒。 |
定义一个继承Thread父类的线程
package com.itheima.d2_demo2;
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
// 为当前的线程对象设置名称,送给父类的有参数构造器初始化名称
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "子线程输出:" + i);
}
}
}
调用该线程
package com.itheima.d2_demo2;
public class ThreadDemo1 {
public static void main(String[] args) throws Exception {
Thread t1 = new MyThread("Thread 1号");
t1.start();
// 获取线程的名字
System.out.println(t1.getName());
Thread t2 = new MyThread("Thread 2号");
t2.start();
// 获取线程的名字
System.out.println(t2.getName());
// 哪个线程执行它,它就得到哪个线程对象(当前的线程对象)
Thread m = Thread.currentThread();
System.out.println(m.getName());
m.setName("main线程");
for (int i = 0; i < 5; i++) {
System.out.println(m.getName() + "输出" + i);
if (i == 3){
// 线程的休眠功能
Thread.sleep(3000);
}
}
}
}
运行结果:
Thread 1号
Thread 2号
Thread 1号子线程输出:0
Thread 1号子线程输出:1
Thread 1号子线程输出:2
Thread 1号子线程输出:3
Thread 1号子线程输出:4
Thread 2号子线程输出:0
Thread 2号子线程输出:1
Thread 2号子线程输出:2
Thread 2号子线程输出:3
Thread 2号子线程输出:4
main
main线程输出0
main线程输出1
main线程输出2
main线程输出3
main线程输出4
# 线程同步
# 线程安全问题
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。
# 线程同步
为了解决线程安全问题。
线程同步的核心思想是: 加锁
,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。
# 同步代码块
作用:
把出现线程安全问题的核心代码给上锁。
原理:
每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。
格式
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}
1.同步代码块是如何实现线程安全的?
- 对出现问题的核心代码使用
synchronized
进行加锁 - 每次只能一个线程占锁进入访问
2.同步代码块的同步锁对象有什么要求?
规范上:建议使用共享资源作为锁对象。
对于
实例方法
建议使用this
作为锁对象。对于
静态方法
建议使用字节码(类名.class
)对象作为锁对象。
# 取钱业务
需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。
分析:
①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类,线程类可以处理账户对象。
③:创建2个线程对象,传入同一个账户对象。
④:启动2个线程,去同一个账户对象中取钱10万。
创建一个对象的类
package com.itheima.d3_thread_safe;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private String cardId;
private int money;
// final修饰后: 锁对象是唯一和不可替代的,非常专业
private final Lock lock = new ReentrantLock();
public Account() {
}
public Account(String cardId, int money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public void drawMoney(double i) {
// 拿到是谁来取钱
String name = Thread.currentThread().getName();
// 线程同步操作
synchronized (this){
// 判断是否有钱
if (this.money >= i){
System.out.println(name + "取钱成功、吐出:" + i);
// 更新余额
money -= i;
System.out.println(name + "取钱成功还剩下:" + this.money);
}else {
// 余额不足
System.out.println(name + "来取钱余额不足取钱失败");
}
}
}
}
创建一个线程,需要继承Thread的父类
package com.itheima.d3_thread_safe;
public class GetMoenyThread extends Thread{
// 定义一个对象
private Account acc;
// 定义一个有参的构造器
public GetMoenyThread(Account acc, String name) {
super(name);
this.acc = acc;
}
@Override
public void run() {
// 调用方法取钱
acc.drawMoney(100000);
}
}
创建一个测试类:
package com.itheima.d3_thread_safe;
public class DrawThread {
public static void main(String[] args) {
// 定义线程类,创建一个共享的账户对象
Account acc = new Account("ICBC-11111",100000);
// 创建2个线程对象,代表小明和小红同时进来了
new GetMoenyThread(acc,"小明").start();
new GetMoenyThread(acc,"小红").start();
}
}
运行结果:
小明取钱成功、吐出:100000.0
小明取钱成功还剩下:0
小红来取钱余额不足取钱失败
# 同步方法
作用:
把出现线程安全问题的核心方法给上锁。
原理:
每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。
格式
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
# Lock锁
为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象。
方法名称 | 说明 |
---|---|
public ReentrantLock() | 获得Lock锁的实现类对象 |
Lock的API
方法名称 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
按照上面的示例修改成上锁,将对象里的取钱方法改为Lock上锁,添加final类型的lock对象:
// final修饰后: 锁对象是唯一和不可替代的,非常专业
private final Lock lock = new ReentrantLock();
public void drawMoney(double i) {
lock.lock(); //上锁
try {
// 拿到是谁来取钱
String name = Thread.currentThread().getName();
// 判断是否有钱
if (this.money >= i){
System.out.println(name + "取钱成功、吐出:" + i);
// 更新余额
money -= i;
System.out.println(name + "取钱成功还剩下:" + this.money);
}else {
// 余额不足
System.out.println(name + "来取钱余额不足取钱失败");
}
} finally {
lock.unlock(); // 解锁
}
}
# 线程池
# 线程池概述
线程池就是一个可以复用线程的技术。
不使用线程池的问题:
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的
,这样会严重影响系统的性能。
谁代表线程池:
JDK 5.0起提供了代表线程池的接口:ExecutorService
# 线程池对象
方式一:
使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
方式二:
使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象
ThreadPoolExecutor构造器的参数说明
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
# 处理Runnable任务
ThreadPoolExecutor创建线程池对象示例
ExecutorService pool = new ThreadPoolExecutor(3,5,
6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
新任务拒绝策略
策略 | 详解 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |
示例:
package com.itheima.d3_threadpool;
import java.lang.annotation.Target;
import java.util.concurrent.*;
public class ThreadPoolDemo1 {
public static void main(String[] args) throws Exception {
// 创建线程池对象
/*
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
*/
ExecutorService pool = new ThreadPoolExecutor(3,5,
6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 给任务线程池子处理
Runnable target = new MyRunnable();
pool.execute(target);
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "输出了:HelloWord ===>" + i);
}
try {
System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,进入休眠");
Thread.sleep(10000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
pool-1-thread-1输出了:HelloWord ===>0
pool-1-thread-1输出了:HelloWord ===>1
pool-1-thread-1输出了:HelloWord ===>2
pool-1-thread-1输出了:HelloWord ===>3
pool-1-thread-1输出了:HelloWord ===>4
pool-1-thread-1本任务与线程绑定了,进入休眠
# 处理Callable任务
ExecutorService的常用方法
方法名称 | 说明 |
---|---|
void execute(Runnable command) | 执行任务/命令,没有返回值,一般用来执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行Callable任务,返回未来任务对象获取线程结果 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
package com.itheima.d3_threadpool;
import java.lang.annotation.Target;
import java.util.concurrent.*;
public class ThreadPoolDemo1 {
public static void main(String[] args) throws Exception {
// 创建线程池对象
/*
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
*/
ExecutorService pool = new ThreadPoolExecutor(3,5,
6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 给任务线程池子处理
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(300));
Future<String> f3 = pool.submit(new MyCallable(400));
Future<String> f4 = pool.submit(new MyCallable(500));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += n;
}
return Thread.currentThread().getName() + "结果是:" + sum;
}
}
运行结果:
pool-1-thread-2结果是:10000
pool-1-thread-3结果是:90000
pool-1-thread-3结果是:160000
pool-1-thread-3结果是:250000
# Executors工具类
Executors得到线程池对象的常用方法
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。
方法名称 | 说明 |
---|---|
public static newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。 |
public static newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
public static newSingleThreadExecutor () | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
public static newScheduledThreadPool(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |
注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。
Executors使用可能存在的陷阱
大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
方法名称 | 存在问题 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 允许请求的任务队列长度是Integer.MAX_VALUE,可能出现OOM错误( java.lang.OutOfMemoryError ) |
public static ExecutorService newSingleThreadExecutor() | |
public static ExecutorService newCachedThreadPool() | 创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长,也可能出现OOM错误( java.lang.OutOfMemoryError ) |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) |
总结:
1.Executors工具类底层是基于什么方式实现的线程池对象?
- 线程池ExecutorService的实现类:ThreadPoolExecutor
2.Executors是否适合做大型互联网场景的线程池方案?
- 不合适。
- 建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。
# 定时器
定时器是一种控制任务延时调用,或者周期调用的技术。
作用:
闹钟、定时邮件发送。
# 定时器的实现方式
方式一:Timer
方式二: ScheduledExecutorService
# Timer定时器
构造器 | 说明 |
---|---|
public Timer() | 创建Timer定时器对象 |
方法 | 说明 |
---|---|
public void schedule(TimerTask task, long delay, long period) | 开启一个定时器,按照计划处理TimerTask任务 |
Timer定时器的特点和存在的问题
1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。
2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。
package com.itheima.d3_thread_timer;
import java.util.Timer;
import java.util.TimerTask;
public class TimerDemo1 {
public static void main(String[] args) {
// 创建Timer定时器
Timer timer = new Timer();
// 调用方法,处理定时任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行一次");
}
},3000,1000);
}
}
# SES定时器
ScheduledExecutorService定时器
ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService内部为线程池。
Executors的方法 | 说明 |
---|---|
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 得到线程池对象 |
ScheduledExecutorService的方法 | 说明 |
---|---|
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 周期调度方法 |
ScheduledExecutorService的优点
1、基于线程池,某个任务的执行情况不会影响其他定时任务的执行。
package com.itheima.d3_thread_timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class TimerDemo2 {
public static void main(String[] args) {
// 创建ScheduledExecutorService线程池,做定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
// 开启定时任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行输出: AAA");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS);
// 添加第二个定时器的目的: 第一个定时器坏了 或者抛出异常依旧继续
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行输出: BBB");
}
},0,2, TimeUnit.SECONDS);
}
}
运行结果:
pool-1-thread-1执行输出: AAA
pool-1-thread-2执行输出: BBB
pool-1-thread-2执行输出: BBB
pool-1-thread-2执行输出: BBB
pool-1-thread-2执行输出: BBB
pool-1-thread-2执行输出: BBB
pool-1-thread-2执行输出: BBB