目录
前言
快速认识多线程
1.基本概念
1.1. 进程和线程
1.2. 同步和异步
1.3. 并行和并发
2.为什么学习多线程
3. 快速创建一个多线程
4. 线程生命周期
5.小结
多线程基础
1. 创建线程
2.守护线程
3.Thread常见API
3.1.线程休眠
3.2.获取线程ID
3.3.线程中断
3.4.线程join
4.小结
前言
本文是《Java并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。
本系列共五篇博客,本篇博客着重聊多线程基础。
侵权删。
快速认识多线程
1.基本概念
1.1. 进程和线程
进程:系统进行资源分配和调度的最小单位;
线程:程序执行的路径,进程中独立执行的子任务,是CPU调度的最小单位。
举例:进程是火车,线程是节节车厢。
1.2. 同步和异步
同步:一旦开始,必须等待方法调用返回后,才能继续后续活动;
异步:方法调用立即、瞬间返回,异步方法会在另一个线程中执行。
举例:商场买冰箱,售货员下单、调配,在商店等候并一起配送回家,此为同步;网上买冰箱,发货、分拣、配送我并不参与,在家想干啥干啥,此为异步。
1.3. 并行和并发
两个或多个任务一起执行,但侧重点不同:
并发:多个任务交替执行,多个任务之间可能串行;
并行:真正意义上同时执行。
高并发:瞬时涌入大量流量,只能负载均衡、主从复制等。
对于并发而言,一会执行任务A、一会执行任务B,系统会不停在二者之间切换,外部观察者而言,即便多任务串行,也会造成并行的错觉。一般来说,真实的并行只能存在于多个CPU或多核CPU内核系统中。
2.为什么学习多线程
为了程序运行的够快。
1. 发挥多处理器的强大能力;
2. 在单处理器系统上获得更高的吞吐量:一个线程等待I/O操作完成,另一个线程可以继续运行,即继续使用处理器;
3. 异步事件的简化处理。
3. 快速创建一个多线程
实现打游戏的同时听音乐。
传统串行方式:
public class Test1 {
//串行处理:仅能执听音乐,无法执行打游戏;
public static void main(String[] args) {
listenMusic();
playGame();
}
private static void listenMusic() {
//持续动作,所以设置为true
while (true) {
System.out.println("听音乐");
}
}
private static void playGame() {
while (true) {
System.out.println("打游戏");
}
}
}
结果输出:
听音乐
听音乐
听音乐
听音乐
听音乐
听音乐
听音乐
多线程技术:
public class Test3 {
//更优雅的写法
public static void main(String[] args) {
new Game().start();
new Music().start();
}
}
class Game extends Thread {
@Override
public void run() {
while (true) {
System.out.println("打游戏");
}
}
}
class Music extends Thread {
@Override
public void run() {
while (true) {
System.out.println("听音乐");
}
}
}
结果输出:
听音乐
听音乐
听音乐
打游戏
打游戏
...
4. 线程生命周期
生命周期包括新建(NEW)、就绪(RUNNABLE)、运行(RUNNING)、阻塞(BLOCKED)和销毁(TERMINATED)。
问题1:调用start方法时,线程会执行吗?
答案:并未执行,线程的运行与否取决于CPU的调度,CPU没有把时间片给你,你就不会执行,因此,线程的执行顺序与start方法顺序并无直接关系。
Thread thread = new Thread();//新建状态
thread.start();//就绪状态
问题2:Runnable会进入Blocked状态么?
答案:不会,只能意外终止或者获取CPU时间片进入Running状态。
问题3:Running的状态转换?
答案:1.满足某个逻辑表示,或者调用stop方法,直接进入Terminated状态;2.调用sleep/wait方法,进入Blocked状态;3.进行某个阻塞的IO操作,进入了Blocked状态;3.时间片用光/执行yield方法(主动放弃),进入Runnable状态。
问题4:Blocked状态转换?
答案:只能进入Runnable或者Terminated状态。stop进入Terminated状态,notify/notifyAll进入Runnable状态,不能直接进入Running状态。
问题5:什么时候会进入Terminated状态?
答案:线程正常执行结束;线程出错意外结束;JVM异常等线程都会结束。
5.小结
本节介绍了进程和线程的概念,同步和异步的概念,多线程的益处,如何快速创建线程,最后介绍了线程的生命周期。
多线程基础
1. 创建线程
//创建线程三种方式:
//1.继承Thread类:
class Music extends Thread {
@Override
public void run() {
//...
}
}
new Music().start();
//疑问:复写的是run方法,执行的时候调用的是start方法?
//答案:start使线程开始执行,Java虚拟机调用该线程的run方法。JNI方法。
//2.实现Runnable接口:复写run方法:解决方法1中Java的单继承问题,更推荐使用。
class Music implements Runnable {
@Override
public void run() {
//...
}
}
new Thread(new Music()).start();
//3.实现callable接口:解决回执问题。
public class Test2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建Shopping
Callable<String> callable = new Shopping();
//使用Future包装callable
FutureTask<String> futureTask = new FutureTask<>(callable);
//创建线程
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("线程启动了");
//调用get之后,线程会进行阻塞,直到获取到线程的执行结果为止。
String s = futureTask.get();
System.out.println("获取执行结果");
System.out.println(s);
}
}
class Shopping implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("空调配送中...");
//模拟空调配送过程的耗时
Thread.sleep(5000);
System.out.println("空调到家啦...");
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "空调送到家啦";
}
}
结果输出:
线程启动了
空调配送中...
空调到家啦...
获取执行结果
2022-06-04 20:11:46空调送到家啦
2.守护线程
守护线程一般处理后台操作,比如jvm的垃圾回收器;main方法中的线程又被称为用户/工作线程。
如果用户线程无事可做了,守护线程要守护的对象不存在了,程序自然而然结束,守护线程会随之销毁。
//setDaemon将thread线程设置该线程为当前线程(main)的守护线程。main线程结束,thread线程亦随之结束。
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
//睡一秒
System.out.println("111111");
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
}
}
}
});
//设置该线程为当前线程(main)的守护线程。
thread.setDaemon(true);
thread.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("main线程结束");
}
}
结果输出:
111111
111111
111111
main线程结束
守护线程一般处理后台操作,有时被称为后台线程。假设不设置守护线程,在程序执行完毕,JVM无法关闭,垃圾回收也会持续进行。eg:游戏程序,有个线程负责与服务端交互,拉取玩家最新金币和经验,假设未设置守护线程,游戏关闭该线程依然与服务端交互,不合适。
3.Thread常见API
3.1.线程休眠
提供了Thread.sleep(XXX);和TimeUnit.HOURS.sleep(XXX);这两个API,更推荐后者,因为后者功能强大,且可读性更高。
public static void main(String[] args) throws InterruptedException {
System.out.println("休眠前");
//使当前线程休眠,静态方法,入参为毫秒数。sleep休眠,线程不会释放锁。
Thread.sleep(1000);
System.out.println("休眠后");
//省去时间单位换算,比如想让线程休眠3h24min17s88ms
//墙裂建议:使用TimeUnit取代 sleep,因为TimeUnit功能全,可读性更高
System.out.println("休眠前");
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(24);
TimeUnit.SECONDS.sleep(17);
TimeUnit.MILLISECONDS.sleep(88);
System.out.println("休眠后");
}
3.2.获取线程ID
线程ID全局唯一,且从零开始。JVM提供了getId()这一API去获取线程。
//获取当前线程
Thread thread = Thread.currentThread();
//获取线程ID
System.out.println("线程ID是:" + thread.getId());
结果输出:
线程ID是:1
问题:main线程的ID序号并非0?
答案:JVM启动时,会开辟多个线程,绝大多数是守护线程,自增序列已得到一定消耗,绝非零号线程。
3.3.线程中断
提供了interrupt、interrupted和isInterrupt这三个API。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//创建一个线程,休眠1min,后输出"休眠结束"。
System.out.println("开始休眠");
try {
TimeUnit.MINUTES.sleep(1);
System.out.println("休眠结束了");
} catch (InterruptedException e) {
System.out.println("线程休眠被打断");
e.printStackTrace();
}
}
});
thread.start();
//主线程调用它的interrupt方法,该线程被中断。
TimeUnit.SECONDS.sleep(5);
thread.interrupt();
}
结果输出:
开始休眠
线程休眠被打断
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:337)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at com.company.unit2.Test5$1.run(Test5.java:13)
at java.base/java.lang.Thread.run(Thread.java:832)
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(5);
System.out.println(thread.isInterrupted());
//中断需要时间去销毁
thread.interrupt();
TimeUnit.SECONDS.sleep(2);
System.out.println(thread.isInterrupted());
}
结果输出:
false
true
//interrupted为Thread静态方法,该方法作用是判断当前线程是否被中断,但当该方法调用后,会直接擦除掉线程的中断标识。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
boolean interrupted = Thread.interrupted();
if (interrupted) {
System.out.println("获取到的结果为true");
}
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();
}
3.4.线程join
一个线程非常依赖另一个线程的输出,这个线程需要等待依赖线程执行完毕,才能继续执行,这时就需要join方法。
public class Test8 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Product());
Thread t2 = new Thread(new Front());
Thread t3 = new Thread(new Back());
t1.start();
//只等待了7000ms
t1.join(7000);
//t1.join();
t2.start();
t3.start();
}
}
class Product implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("产品整理需求中...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Front implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("前端开发,正在写页面...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Back implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("后端开发,正在写接口...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果输出:
产品整理需求中...
产品整理需求中...
产品整理需求中...
前端开发,正在写页面...
后端开发,正在写接口...
前端开发,正在写页面...
后端开发,正在写接口...
后端开发,正在写接口...
前端开发,正在写页面...
产品整理需求中...
后端开发,正在写接口...
前端开发,正在写页面...
后端开发,正在写接口...
前端开发,正在写页面...
产品整理需求中...
后端开发,正在写接口...
前端开发,正在写页面...
后端开发,正在写接口...
前端开发,正在写页面...
可以看到,t1线程启动后,又调用了t1的join方法,这样main线程就会等待t1线程执行完毕后,才会继续启动t2和t3线程。
4.小结
本节介绍了三种方式创建线程:单继承Thread不够灵活;实现Runnable接口更常用;实现Callable接口适用于有返回值的情况。
守护线程一般处理后台操作,假设要守护的对象不存在了,程序自然而然结束。
常用的API:休眠用TimeUnit.SECONDS.sleep();获取线程ID用Thread.currentThread().getId();中断用thread.interrupt();假设B线程需要等待A线程执行完毕,使用thread.join(XXX)。