目录
多线程的实现方案三:利用Callable、FutureTask接口实现
视频教程传送门 -> https://www.bilibili.com/video/BV1Cv411372m?p=164
多线程的创建
方式一:继承Thread类
方式二:实现Runnable接口
方式三:JDK 5.0新增:实现Callable接口
Thread的构造器
public Thread(String name) -> 可以为当前线程指定名称
public Thread(Runnable target) -> 封装Runnable对象成为线程对象
public Thread(Runnable target ,String name ) -> 封装Runnable对象成为线程对象,并指定线程名称
多线程的实现方案一:继承Thread类
Java是通过java.lang.Thread 类来代表线程的,Thread类提供了实现多线程的方式
步骤:
定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
创建MyThread类的对象
调用线程对象的start()方法启动线程(启动后还是执行run方法的)
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展
注意:
1.为什么不直接调用了run方法,而是调用start启动线程?
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
2、为什么把主线程任务放在子线程之后
把主线程任务放在子线程之前的话,主线程先跑完,相当于是一个单线程的效果了。
package com.test.d1_create;
/**
多线程的创建方式一:继承Thread类实现。
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程类继承Thread类
*/
class MyThread extends Thread{
/**
2、重写run方法,里面是定义线程以后要干什么
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
输出示例:
多线程的实现方案二:实现Runnable接口
步骤:
定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
创建MyRunnable任务对象
把MyRunnable任务对象交给Thread处理
调用线程对象的start()方法启动线程
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的
package com.test.d1_create;
/**
线程的创建方式二,理解它的优缺点。
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable {
/**
2、重写run方法,定义线程的执行任务的
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
多线程的实现方案三:利用Callable、FutureTask接口实现
前2种线程创建方式存的问题:
重写的run方法均不能直接返回结果,不适合需要返回线程执行结果的业务场景
步骤:
得到任务对象
定义类实现Callable接口,重写call方法,封装要做的事情
用FutureTask把Callable对象封装成线程任务对象
把线程任务对象交给Thread处理
调用Thread的start方法启动线程,执行任务
线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果
FutureTask的API
public FutureTask<>(Callable call) -> 把Callable对象封装成FutureTask对象
public V get() throws Exception -> 获取线程执行call方法返回的结果
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;
可以在线程执行完毕后去获取线程执行的结果
缺点:编码复杂一点
package com.test.d1_create;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
线程的创建方式三:实现Callable接口,结合FutureTask完成
*/
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call = new MyCallable(10);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask<String> f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable<String> call2 = new MyCallable(20);
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum;
}
}
输出:
三种方式对比
方式 | 优点 | 缺点 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他的类; 不能返回线程执行的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能返回线程执行的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类; 可以得到线程执行的结果 | 编程相对复杂 |
Thread的常用方法
String getName() -> 获取当前线程的名称,默认线程名称是Thread-索引
void setName(String name) -> 设置线程名称
public static Thread currentThread() -> 返回对当前正在执行的线程对象的引用
public static void sleep(long time) -> 让线程休眠指定的时间,单位为毫秒
public void run() -> 线程任务方法
public void start() -> 线程启动方法
【例】获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()
MyThread.java
package com.test.d2_api;
public class MyThread extends Thread{
public MyThread() {
}
public MyThread(String name) {
// 为当前线程对象设置名称,送给父类的有参数构造器初始化名称
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println( Thread.currentThread().getName() + "输出:" + i);
}
}
}
ThreadDemo01.java
package com.test.d2_api;
public class ThreadDemo01 {
// main方法是由主线程负责调度的
public static void main(String[] args) {
Thread t1 = new MyThread("1号线程");
// t1.setName("1号");
t1.start();
System.out.println("第一个子线程名称设置的是"+t1.getName());
Thread t2 = new MyThread("2号线程");
// t2.setName("2号");
t2.start();
System.out.println("第二个子线程名称设置的是"+t2.getName());
// 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
// 主线程的名称就叫main
Thread m = Thread.currentThread();
System.out.println("主线程默认的名称是"+m.getName());
m.setName("最牛的线程");
System.out.println("主线程新的名称是"+m.getName());
for (int i = 0; i < 3; i++) {
System.out.println( m.getName() + "输出:" + i);
}
}
}
输出示例 =>
第一个子线程名称设置的是1号线程
第二个子线程名称设置的是2号线程
主线程默认的名称是main
主线程新的名称是最牛的线程
最牛的线程输出:0
2号线程输出:0
1号线程输出:0
2号线程输出:1
最牛的线程输出:1
2号线程输出:2
1号线程输出:1
最牛的线程输出:2
1号线程输出:2
【例】sleep()
package com.test.d2_api;
import java.util.Date;
/**
目标:线程的API
*/
public class ThreadDemo02 {
// main方法是由主线程负责调度的
public static void main(String[] args) throws Exception {
for (int i = 1; i <= 5; i++) {
System.out.println(new Date() + " 输出:" + i);
if(i == 3){
// 让当前线程进入休眠状态
// 段子:项目经理让我加上这行代码,如果用户愿意交钱,我就注释掉。
Thread.sleep(3000);
}
}
}
}
线程同步
线程安全问题
多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题
=> 例如银行取钱
线程安全问题出现的原因?
存在多线程并发
同时访问共享资源
存在修改共享资源
解决方法 =>
加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其它线程才能进来
方式一:同步代码块
方式二:同步方法
方式三:Lock锁
同步代码块
对出现问题的核心代码使用synchronized进行加锁 => 每次只能一个线程占锁进入访问
synchronized(同步锁对象) {
操作共享资源的代码(核心代码)
}
锁对象的规范要求
对于实例方法建议使用this作为锁对象
对于静态方法建议使用字节码(类名.class)对象作为锁对象
public class Account {
private String cardId;
private double money; // 余额 关键信息
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
……
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void drawMoney(double money) {
// 1、拿到是谁来取钱
String name = Thread.currentThread().getName();
// 同步代码块
synchronized (this) {
// 2、判断余额是否足够
if(this.money >= money){
// 钱够
System.out.println(name+"来取钱,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱后,余额剩余:" + this.money);
}else{
// 3、余额不足
System.out.println(name+"来取钱,余额不足!");
}
}
}
}
同步方法
对出现问题的核心方法使用synchronized修饰 => 每次只能一个线程占锁进入访问
修饰符 synchronized 返回值类型 方法名称(形参列表) {
操作共享资源的代码
}
同步代码块锁的范围更小,同步方法锁的范围更大
同步方法的同步锁对象的原理
对于实例方法默认使用this作为锁对象
对于静态方法默认使用类名.class对象作为锁对象
public class Account {
private String cardId;
private double money; // 余额 关键信息
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
……
public synchronized void drawMoney(double money) {
// 1、拿到是谁来取钱
String name = Thread.currentThread().getName();
// 2、判断余额是否足够
if(this.money >= money){
// 钱够
System.out.println(name+"来取钱,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱后,余额剩余:" + this.money);
}else{
// 3、余额不足
System.out.println(name+"来取钱,余额不足!");
}
}
}
Lock锁(JDK5以后)
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象
public ReentrantLock() -> 获得Lock锁的实现类对象
void lock() -> 获得锁
void unlock() -> 释放锁
public class Account {
private String cardId;
private double money; // 余额 关键信息
// final修饰后:锁对象是唯一和不可替换的,非常专业
private final Lock lock = new ReentrantLock();
public Account() {
}
public Account(String cardId, double money) {
this.cardId = cardId;
this.money = money;
}
……
public void drawMoney(double money) {
// 1、拿到是谁来取钱
String name = Thread.currentThread().getName();
// 2、判断余额是否足够
lock.lock(); // 上锁
try {
if(this.money >= money){
// 钱够
System.out.println(name+"来取钱,吐出:" + money);
// 更新余额
this.money -= money;
System.out.println(name+"取钱后,余额剩余:" + this.money);
}else{
// 3、余额不足
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)
参数一:指定线程池的线程数量(核心线程): corePoolSize
参数二:指定线程池可支持的最大线程数: maximumPoolSize -> 最大数量 >= 核心线程数量
参数三:指定临时线程的最大存活时间: keepAliveTime
参数四:指定存活时间的单位(秒、分、时、天): unit
参数五:指定任务队列: workQueue
参数六:指定用哪个线程工厂创建线程: threadFactory
参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler
临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
什么时候会开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
ThreadPoolExecutor创建线程池对象示例 -->
ExecutorService pools = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), 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()方法从而绕过线程池直接执行 |
【例】线程池处理Runnable任务
MyRunnable.java
package com.test.d8_threadpool;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld ==> " + i);
}
try {
System.out.println(Thread.currentThread().getName() + "本任务与线程绑定了,线程进入休眠了zZZ~");
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
ThreadPoolDemo1.java
package com.test.d8_threadpool;
import java.util.concurrent.*;
public class ThreadPoolDemo1 {
public static void main(String[] args) {
// 1、创建线程池对象
/**
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<>(4) , Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() );
// 2、给任务线程池处理。
Runnable target = new MyRunnable();
pool.execute(target);
pool.execute(target);
pool.execute(target);
// 新来的任务进入任务队列
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);
// 创建临时线程(新任务提交时发现核心线程都在忙,任务队列也满了)
pool.execute(target);
pool.execute(target);
// 不创建,触发拒绝策略
//pool.execute(target);
// 关闭线程池(开发中不建议使用)
// pool.shutdownNow(); // 立即关闭,即使任务没有完成,会丢失任务的!
pool.shutdown(); // 会等待全部任务执行完毕之后再关闭(建议使用)
}
}
39行如果不注释,饱和后会丢弃任务并抛出RejectedExecutionException异常
注释39行后,输出如下,箭头处5个线程忙完了第一波继续处理队列里的任务
【例】MyCallable.java
package com.test.d8_threadpool;
import java.util.concurrent.Callable;
/**
1、定义一个任务类 实现Callable接口 应该申明线程任务执行完毕后的结果的数据类型
*/
public class MyCallable implements Callable<String>{
private int n;
public MyCallable(int n) {
this.n = n;
}
/**
2、重写call方法(任务方法)
*/
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n ; i++) {
sum += i;
}
return Thread.currentThread().getName()
+ "计算 1-" + n+ "的和,结果是:" + sum;
}
}
ThreadPoolDemo2.java
package com.test.d8_threadpool;
import java.util.concurrent.*;
public class ThreadPoolDemo2 {
public static void main(String[] args) throws Exception {
// 1、创建线程池对象
/**
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() );
// 2、给任务线程池处理。
Future<String> f1 = pool.submit(new MyCallable(10));
Future<String> f2 = pool.submit(new MyCallable(20));
Future<String> f3 = pool.submit(new MyCallable(30));
Future<String> f4 = pool.submit(new MyCallable(40));
Future<String> f5 = pool.submit(new MyCallable(50));
Future<String> f6 = pool.submit(new MyCallable(60));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
System.out.println(f5.get());
System.out.println(f6.get());
pool.shutdown();
}
}
输出:
pool-1-thread-1计算 1-10的和,结果是:55
pool-1-thread-2计算 1-20的和,结果是:210
pool-1-thread-3计算 1-30的和,结果是:465
pool-1-thread-2计算 1-40的和,结果是:820
pool-1-thread-3计算 1-50的和,结果是:1275
pool-1-thread-2计算 1-60的和,结果是:1830
Executors工具类实现线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
使用存在风险,堆积大量请求 / 创建大量线程 --> OOM
并行、并发、线程的生命周期
并发:CPU分时轮询的执行线程
并行:同一个时刻同时在执行
线程的6种状态
线程状态 | 描述 |
New(新建) | 线程刚被创建,但是并未启动 |
Runnable(可运行) | 线程已经调用了start()等待CPU调度 |
Blocked(锁阻塞) | 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
Waiting(无限等待) | 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep 、Object.wait |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |