0
点赞
收藏
分享

微信扫一扫

并发编程入门(一):多线程基础


目录

​​前言​​

​​快速认识多线程​​

​​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并发视频入门》视频课程的笔记总结,旨在帮助更多同学入门并发编程。

本系列共五篇博客,本篇博客着重聊多线程基础。

侵权删。

并发编程入门(一):多线程基础_多线程

https://www.bilibili.com/video/BV1a5411u7o7?p=1

快速认识多线程

1.基本概念

1.1. 进程和线程

进程:系统进行资源分配和调度的最小单位;

线程:程序执行的路径,进程中独立执行的子任务,是CPU调度的最小单位。

举例:进程是火车,线程是节节车厢。

1.2. 同步和异步

同步:一旦开始,必须等待方法调用返回后,才能继续后续活动;

异步:方法调用立即、瞬间返回,异步方法会在另一个线程中执行。

举例:商场买冰箱,售货员下单、调配,在商店等候并一起配送回家,此为同步;网上买冰箱,发货、分拣、配送我并不参与,在家想干啥干啥,此为异步。

并发编程入门(一):多线程基础_java_02

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)。

并发编程入门(一):多线程基础_并发_03

问题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状态。

并发编程入门(一):多线程基础_开发语言_04

问题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)。

举报

相关推荐

0 条评论