多线程
一、线程
1、概述
-
线程(thread)是一个程序内部的一条执行路径。
-
我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。
-
程序中如果只有一条执行路径,那么这个程序就是单线程的程序。
二、多线程
1、概述
- 多线程是指从软硬件上实现多条执行流程的技术。
2、好处
-
举例说明:
- 1、比如网上购票,肯定是一堆用户在购票吧?所以在购票系统后台就需要多条线程来处理多个用户的购票请求。
- 2、比如你在百度网盘,上传文件的同时有没有可能同时也在下载文件?所以在后台也需要多条线程来处理这些请求。
- 3、再例如:消息通信、淘宝、京东系统都离不开多线程技术。
3、关于多线程需要学会什么?
(1)多线程的创建
- 如何在程序中实现多线程,有哪些方式,各自有什么优缺点。
(2)Thread类的常用方法
- 线程的代表是Thread类,Thread提供了哪些线程的操作给我们呢?
(3)线程安全、线程同步
- 多个线程同时访问一个共享的数据的时候会出现问题,如何去解决?
(4)线程通信、线程池
- 线程与线程间需要配合完成一些业务。
- 线程池是一种线程优化方案,可以用一种更好的方式使用多线程。
(5)定时器、线程状态等
- 如何在程序中实现定时器?
- 线程在执行的过程中会有很多不同的状态,理解这些状态有助于理解线程的执行原理,也有利于面试。
4、多线程的创建
Thread类
- Java是通过java.lang.Thread类来代表线程的。
- 按照面向对象的思想,Thread类应该提供了实现多线程的方式。
(1)方式一
-
继承Thread类
- 1、定义一个子类MyThread继承线程类
java.lang.Thread
,重写run()
方法 - 2、创建MyThread类的对象
- 3、调用线程对象的
start()
方法启动线程(启动后还是执行run()
方法的)
package com.app.d1_create_thread; /** 1、定义一个MyThread类,继承Thread类 */ public class MyThread extends Thread { /** 重写run方法,里面是定义线程的任务 */ @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println("第 " + i + " 个子线程在执行输出!"); } } }
package com.app.d1_create_thread; /** 目标:多线程的创建方式一:继承Thread类 */ public class ThreadDemo01 { public static void main(String[] args) { // 2、创建MyThread类的对象 Thread t = new MyThread(); // 3、调用线程对象的start()方法启动线程(执行的还是run()方法) t.start(); for (int i = 1; i <= 3; i++) { System.out.println("第 " + i + " 个主线程在执行输出~"); } } }
第一次运行:主、子线程会同时执行,顺序时随机的
第 1 个主线程在执行输出~ 第 1 个子线程在执行输出! 第 2 个子线程在执行输出! 第 2 个主线程在执行输出~ 第 3 个主线程在执行输出~ 第 3 个子线程在执行输出! Process finished with exit code 0
第二次运行:主、子线程会同时执行,顺序时随机的
第 1 个主线程在执行输出~ 第 2 个主线程在执行输出~ 第 3 个主线程在执行输出~ 第 1 个子线程在执行输出! 第 2 个子线程在执行输出! 第 3 个子线程在执行输出! Process finished with exit code 0
- 1、定义一个子类MyThread继承线程类
方式一的优缺点:
- 优点:编码简单。
- 缺点:线程类已继承了Thread,无法再继承其他类,不利于扩展,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)。
重点问题?
1、为什么不直接调用run()方法,而是调用start()启动线程?
-
直接调用run()方法会被当成普通方法执行,此时相当于还是单线程执行。
-
只有调用start()方法才是启动一个新的线程执行(就是告诉CPU,run()方法是要以线程的方式启动)。
-
第一次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。
-
第二次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。
-
第三次运行:直接调用run()方法被当成普通方法执行,此时相当于还是单线程执行。
2、为什么不能把主线程任务放在子线程之前?
-
这样主线程一直是先跑完的,相当于还是单线程执行。
-
第一次运行:这样主线程一直是先跑完的,相当于还是单线程执行
-
第二次运行:这样主线程一直是先跑完的,相当于还是单线程执行
-
第三次运行:这样主线程一直是先跑完的,相当于还是单线程执行
方式一总结
1、方式一是如何实现多线程的?
- 继承Thread类
- 重写run()方法
- 创建线程对象
- 调用start()方法启动
2、方式一的优缺点是什么?
- 优点:编码简单
- 缺点:存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展
(2)方式二
Thread的构造器:
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装Runnable对象成为线程对象 |
public Thread(Runnable target, String name) | 封装Runnable对象成为线程对象,并指定线程名称 |
-
实现Runnable接口
- 1、定义一个线程类MyRunnable实现
Runnable
接口,重写run()
方法 - 2、创建MyRunnable任务对象
- 3、把MyRunnable任务对象交给
Thread
处理 - 4、调用线程对象的
start()
方法启动线程
package com.app.d1_create_thread; /** 1、定义一个线程任务类MyRunnable,实现Runnable接口 */ public class MyRunnable implements Runnable { /** 重写run()方法,里面是定义线程的任务 */ @Override public void run() { for (int i = 0; i < 6; i++) { System.out.println("子线程执行输出:" + i); } } }
package com.app.d1_create_thread; /** 目标:多线程的创建方式二,实现Runnable接口 */ public class ThreadDemo02 { public static void main(String[] args) { // 2、创建一个任务对象 Runnable target = new MyRunnable(); // 3、把任务对象交给Thread处理 Thread t = new Thread(target); // 4、调用线程对象的start()方法启动线程(执行的还是run方法) t.start(); // 主线程任务 for (int i = 0; i < 6; i++) { System.out.println("主线程执行输出:" + i); } } }
第一次运行:主、子线程会同时执行,顺序时随机的
第二次运行:主、子线程会同时执行,顺序时随机的
- 1、定义一个线程类MyRunnable实现
方式二的优缺点
- 优点:线程任务类只是实现接口,可以继续继承其他类和实现其他接口,扩展性强。
- 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)。
方式二总结
1、方式二如何实现创建线程的?
- 定义一个线程任务类实现Runnable接口
- 重写run()方法
- 创建任务对象
- 把任务对象交给Thread(线程对象)处理
- 调用线程对象的start()方法启动线程
2、方式二的优缺点是什么?
- 优点:线程任务类由于只是实现接口,因此可以继续继承一个其他类和实现多个其他接口,扩展性强。
- 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的(因为重写的run方法的返回值类型是void:无返回值)。
(3)方式二的其他语法形式
-
匿名内部类方式实现
- 1、创建一个任务对象
- 2、把任务对象交给Thread(线程对象)处理
- 3、调用线程对象的start()方法启动线程
package com.app.d1_create_thread; /** 目标:多线程创建方式二的其他语法形式(匿名内部类方式实现) */ public class ThreadDemo02Other { public static void main(String[] args) { // 1、创建一个任务对象 Runnable target = new Runnable() { @Override public void run() { for (int i = 0; i < 4; i++) { System.out.println("子线程1执行输出:" + i); } } }; // 2、把任务对象交给Thread(线程对象)处理 Thread t = new Thread(target); // 3、调用线程对象的start()方法启动线程 t.start(); // 简化1: Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 4; i++) { System.out.println("子线程2执行输出:" + i); } } }); t2.start(); // 简化2: new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 4; i++) { System.out.println("子线程3执行输出:" + i); } } }).start(); // 简化3(Lambada): new Thread(() -> { for (int i = 0; i < 4; i++) { System.out.println("子线程4执行输出:" + i); } }).start(); // 主线程任务 for (int i = 0; i < 4; i++) { System.out.println("主线程执行输出:" + i); } } }
第一次运行:主、子线程会同时执行,顺序是随机的
子线程1执行输出:0 子线程2执行输出:0 主线程执行输出:0 子线程4执行输出:0 主线程执行输出:1 主线程执行输出:2 子线程2执行输出:1 子线程1执行输出:1 子线程1执行输出:2 子线程1执行输出:3 子线程2执行输出:2 子线程2执行输出:3 主线程执行输出:3 子线程3执行输出:0 子线程4执行输出:1 子线程4执行输出:2 子线程4执行输出:3 子线程3执行输出:1 子线程3执行输出:2 子线程3执行输出:3 Process finished with exit code 0
第二次运行:主、子线程会同时执行,顺序是随机的
主线程执行输出:0 子线程2执行输出:0 子线程2执行输出:1 子线程4执行输出:0 子线程4执行输出:1 子线程4执行输出:2 子线程4执行输出:3 子线程2执行输出:2 子线程2执行输出:3 子线程1执行输出:0 子线程3执行输出:0 子线程3执行输出:1 子线程3执行输出:2 子线程3执行输出:3 主线程执行输出:1 子线程1执行输出:1 子线程1执行输出:2 主线程执行输出:2 子线程1执行输出:3 主线程执行输出:3 Process finished with exit code 0
(4)方式三
a. 重点问题?
1、前两种线程的创建方式都存在一个问题?
- 它们重写的run()方法均不能直接返回结果。
- 不适合需要返回线程执行结果的业务场景。
2、怎么解决这个问题呢?
JDK 5.0
提供了Callable
和FutureTask
来实现。- 这种方式的优点是:可以得到线程执行的结果。
b. FutureTask的API:
方法名称 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象 |
public V get() throws Exception | 获取线程执行call方法返回的结果 |
c. 利用Callable、FutureTask接口实现:
1、得到任务对象:
- a. 定义线程任务类实现
Callable
接口,重写call()
方法,封装线程的任务。 - b. 创建
Callable
对象。 - c. 用
FutureTask
把Callable
对象封装成线程任务对象。
2、把线程任务对象交给Thread(线程对象)
处理。
3、调用线程对象的start()
方法启动线程,执行任务。
4、线程执行完毕后,通过FutureTask
的get()
方法去获取线程任务执行的结果。
package com.app.d1_create_thread;
import java.util.concurrent.Callable;
/**
1、定义一个线程任务类MyCallable,实现Callable接口
应该声明线程任务执行完毕后结果的数据类型:目前用的是String
*/
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; // 用于累加 1-n的和
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程执行的结果是:" + sum; // 返回线程任务执行完毕后的结果
}
}
package com.app.d1_create_thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
目标:线程的创建方式三,实现Callable接口,结合FutureTask的get方法去获取线程任务执行的结果
*/
public class ThreadDemo03 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable<String> call1 = new MyCallable(10); // 求1-10的和
/**
4、用FutureTask把Callable对象封装成线程任务对象:
作用1:
因为Callable无法直接交给Thread处理,
FutureTask实现了RunnableFuture接口,RunnableFuture接口又继承了Runnable接口,
所以把Callable任务对象交给FutureTask对象,这样就可以把FutureTask对象交给Thread处理。
作用2:
可以在线程任务执行完毕之后,通过调用其get()方法得到线程任务执行完成的结果。
*/
FutureTask<String> f1 = new FutureTask<>(call1);
// 5、把线程任务对象交给Thread(线程对象)处理
Thread t1 = new Thread(f1);
// 6、调用线程对象的start()方法启动线程,执行任务
t1.start();
Callable<String> call2 = new MyCallable(100); // 求1-10的和
FutureTask<String> f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
// 7、线程执行完毕后,通过FutureTask的get()方法,获取线程任务执行的结果
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到f1(线程1)执行完毕才提取结果
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到f2(线程2)执行完毕才提取结果
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
第一个结果:子线程执行的结果是:55
第二个结果:子线程执行的结果是:5050
Process finished with exit code 0
d. 方式三的优缺点:
- 优点:
- 线程任务类只是实现接口,可以继续继承一个其他类,实现多个其他接口,扩展性强。
- 可以在线程任务执行完毕后去获取线程执行的结果。
- 缺点:
- 编码复杂一点。
(5)三种方式总结
1、三种方式对比如何?
方式 | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 扩展性较差,不能再继承其他类, 不能返回线程任务执行完成的结果 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂一点, 不能返回线程任务执行完成的结果 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类, 可以得到线程执行完成的结果 | 编程相对复杂 |