6.Java多线程详解:Thread类、线程属性与start()方法深度解析
目录
Thread 类其他的属性和方法~~
Thread 构造方法概览:
name 属性说明:
Thread 的几个常见属性
前台线程 vs 后台线程
启动一个线程 - start()
中断一个线程
启动一个线程 - start()
变量捕获与 lambda / 匿名内部类
Thread 提供更靠谱的方案实现上述的效果
日常开发中 catch 中的逻辑一般不会这么写~~
Thread 类其他的属性和方法~~
Thread 构造方法概览:
Thread()创建线程对象。
Thread(Runnable target)使用 Runnable 对象创建线程对象。
Thread(String name)创建线程对象,并命名。
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名。
【了解】
Thread(ThreadGroup group, Runnable target)使用 Runnable 对象创建线程对象,并分组。
说明:线程组可以方便批量管理,分组的组别为线程组。多用在底层代码开发中。
批注:
name给线程起名字——不影响线程的执行,方便调试~~
name 属性说明:
默认线程名字为Thread-数字(如Thread-0,Thread-1等)。
代码示例:
Thread t3 = new Thread(() -> { while (true) { System.out.println("t3"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, "t3");Thread 的几个常见属性
属性 | 获取方法 | 说明 |
|---|---|---|
ID |
| ID 是线程的唯一标识,不同线程不会重复 |
名称 |
| 全是给程序员调试工具用 |
状态 |
| 状态表示线程所处的某个情况,下面我们会进一步说明 |
优先级 |
| 优先级高的线程理论上更容易被调度到 |
是否后台线程 |
| 关于后台线程,再说后一点。JVM 会在所有程序的所有后台线程结束后,才会结束运行。是否后台,但简单的理解,为 true 方法是否运行起来 |
是否存活 |
| 线程的中断问题,下面我们进一步说明 |
补充说明:
线程组(ThreadGroup)现在很少涉及到,后续介绍“线程池”生态位上替代了线程组~~
main线程包括代码中手动创建的线程,默认都是前台线程~~返回 0 表示进程执行成功,非 0 值表示失败,使用不同的值表示不同的错误原因~~
学习 C 语言的时候,
hello world中的int main() { return 0; },进程结束返回码~~
前台线程 vs 后台线程
前台线程:会阻止整个进程结束。
举例:李局长要是撤了,宴席就会结束了(主角退场)。
后台线程:不会阻止整个进程结束。
举例:如果我吃饱了,我说我要撤了,是否会使宴席结束呢?——后台线程~~
一个进程中的前台线程,不止有一个~~
得是所有的前台线程都结束了,进程才结束~~
后台线程:也叫做“守护线程”
“默默”背后默默的守护~~
“等你下课” 🎵
启动一个线程 - start()
start操作本质上会调用操作系统提供的 API,在操作系统的内部(内核)创建出一个线程出来了。
private native void start0();Java 的代码 =>
.class,再经过 JVM 解释执行的~~带有
native字样的方法,就是在 JVM 内部通过 C++ 实现的方法。调用操作系统的原生 API 创建线程,根据当前的操作系统做区分~~
内部是一系列的
#if、#ifdef、#else if、#else(C/C++来说)操作系统内部,通过PCB 来描述,通过链表组织~~
对于 Linux 来说(Linux 开源,Windows 闭源)
“被调度起来后执行的逻辑,就是 run 里面设定的刚刚逻辑~~”
图示说明:
操作系统拿着 PCB 结构体来进行调度执行~~
pid:每个 PCB 对应一个线程,在 PCB 上有一个特质的id属性,这个id是相同同的,也就是一个进程~~
start 方法本身,执行速度非常快~~
start针对一个 thread 对象,只能调用一次~~Java 设定了,要让一个 thread 对象和一个操作系统的线程一一对应~~
中断一个线程
英文术语:
Interrupt—— 我个人更喜欢称为“终止”
对于 Java 来说,一个线程终止,就是这个线程的入口方法执行完毕~~
Java 并不提供“强制终止”(所有的让线程终止的做法,都需要等待着“让入口方法结束”)
有些危险:
A 线程调用方法,终止 B 线程。
调用 A 的终止方法时,无法确定 B 线程当前执行到哪个环节了~~
做某个事情,做了一半,就被强制终止了~~
启动一个线程 - start()
之前我们已经看到了如何通过重写run方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
重写
run方法是提供给线程要做的事情的命令清单。线程对象可以认为是把 李四、王五叫过来了。
而调用
start()方法,就是喊一声:“劳动啦!”,线程才真正独立去执行了。
流程图说明:
学员 A(新线程):执行逻辑、响应主线程的通知、A 线程结束。
老师(主线程):开始新线程、去查看任务结果、等待 A 线程结束、汇总到评分系统。
调用 start 方法,才真的在操作系统的层面创建出一个线程。
变量捕获与 lambda / 匿名内部类
写作局部变量,此时无法编译通过~~
变量捕获:lambda / 匿名内部类
捕获的变量,必须是
final/ 事实 final
错误示例:
boolean running = true; Thread t = new Thread(() -> { while (running) { // 编译错误:Variable used in lambda expression should be final or effectively final System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t 线程退出"); }); t.start();写作成员变量,情况就不一样~~
触发的做法不再是“变量捕获”,而是“内部变量向外部变成了类”。
正确示例(使用成员变量):
private static boolean running = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (running) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t 线程退出"); }); t.start(); // 主线程中,让用户选择是否让 t 线程终止 Scanner sc = new Scanner(System.in); System.out.println("请输入整数,0 表示不让 t 线程终止:"); int n = sc.nextInt(); if (n == 0) { running = false; } }说明:
lambda 本质上是基于函数式接口的匿名内部类。
C++ 的考虑:程序自行负责保证变量的生命周期匹配~~
Java 的考虑:变量的生命周期由 JVM 托管,更加安全~~
一旦修改了局部变量的生命周期可能出现不一致,代码更乱。
为了避免混乱,Java 直接禁止修改~~
Thread 提供更靠谱的方案实现上述的效果
获取当前线程的引用:
这个 lambda 在哪个线程中执行的,得到的引用就是哪个线程
Thread引用~~
while (!Thread.currentThread().isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("t 线程退出");示例完整代码:
package thread; import java.util.Scanner; public class Demo9 { public static void main(String[] args) { Thread t = new Thread(() -> { Thread cur = Thread.currentThread(); while (!cur.isInterrupted()) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); t.start(); Scanner scanner = new Scanner(System.in); System.out.println("请输入整数表示让线程 t 结束:"); int n = scanner.nextInt(); if (n == 0) { // 这个方法不光可以设置标志位,还能唤醒 sleep 等导致线程阻塞的方法 // 会使 sleep 漏出异常,InterruptedException t.interrupt(); } } }执行逻辑说明:
执行这个代码的时候,一定是先针对
Thread t中的参数,先进行求值。在执行
Thread的构造方法。在把
Thread构造方法的结果,赋值给t(定义 + 符合)
图示说明:
Thread cur = Thread.currentThread();
this表示当前对象
currentThread表示当前线程
Thread 对象内部,封装了一个 boolean 变量
效果:
这个和手动修改标志位类似。
还额外做了其他操作,把
sleep这样的方法唤醒。
异常信息(将来常用):
Exception in thread "t 线程" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted at thread.Demo9.lambda$main$0(Demo9.java:14) at java.lang.Thread.run(Thread.java:748) Caused by: java.lang.InterruptedException: sleep interrupted at java.base/java.lang.Thread.sleep(Native Method) at thread.Demo9.lambda$main$0(Demo9.java:11) ... 1 moreProcess finished with exit code 0
日常开发中 catch 中的逻辑一般不会这么写~~
IDEA 生成的爆栈代码~~
try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); }实现开发中 catch 根据阻塞的不同,采取不同的动作:
尝试重试~~
记录错误 / 日志~~(记到日志文件,写入到日志服务器中)
触发报警 / 短信~~(钉钉群、企业微信、邮件...)
