目录
[二、Java 中的线程 和 操作系统线程 的关系](#二、Java 中的线程 和 操作系统线程 的关系)
[3.1 继承 Thread,重写 run](#3.1 继承 Thread,重写 run)
[3.2 实现 Runnable,重写 run](#3.2 实现 Runnable,重写 run)
[3.3 基于3.1,使用匿名内部类](#3.3 基于3.1,使用匿名内部类)
[3.4 基于3.2,使用匿名内部类](#3.4 基于3.2,使用匿名内部类)
[3.5 【推荐】使用 lambda 表达式](#3.5 【推荐】使用 lambda 表达式)
[3.6 使用 jconsole 命令观察线程](#3.6 使用 jconsole 命令观察线程)
[3.7 Thread 常见构造方法](#3.7 Thread 常见构造方法)
[四、Thread 的常见属性](#四、Thread 的常见属性)
[4.1 获取当前线程的引用](#4.1 获取当前线程的引用)
[4.2 后台线程](#4.2 后台线程)
[4.3 是否存活](#4.3 是否存活)
[4.4 是否中断](#4.4 是否中断)
[4.5 等待一个线程](#4.5 等待一个线程)
[4.6 休眠线程](#4.6 休眠线程)
一、认识线程
线程的产生:由于单核 CPU 的发展遇到瓶颈,为了提高算力,就出现了多核 CPU。并发编程能够更充分地利用多核 CPU。而并发编程就是需要多个线程"同时"执行多份代码。
虽然多进程也能实现并发编程,但是线程比进程更轻量。(--->线程与进程的区别)
二、Java 中的线程 和 操作系统线程 的关系
线程是 CPU 调度和执行的最小单位,是操作系统中的概念。操作系统内核实现了线程这种机制,并提供了一些 API 供用户使用。
但是操作系统提供的原生线程 API 是由 C 编写的,因此 Java 对操作系统提供的 API 进行了封装,并导入到标准库 (java.lang.)中,也就是 Thread 类。
因为 java.lang. 中所有类是默认导入到 Java 中的,因此使用 Thread 类时,不需要 import。
三、创建线程
创建线程有以下5种方法:
- 继承 Thread 类,重写 run 方法;
- 实现 Runnable 接口,重写 run();
- 继承 Thread,重写 run 方法,使用匿名内部类;
- 实现 Runnable,重写 run(),使用匿名内部类;
- 使用 lambda 表达式。
PS:当我们点进 Thread 原代码时,发现 Thread 实现了 Runnable 接口,而 Runnable 接口中只有一个 run 方法:

3.1 继承 Thread,重写 run
java
class myThread extends Thread{
@Override
public void run() {
// 使用while进行循环,方便查看效果
while (true){
System.out.println("Hello thread");
// 使用 sleep() 必须抛异常
// sleep 是静态方法,作用是休眠,让当前线程暂时放弃CPU资源
try {
// 单位是毫秒,1000ms就是1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
// 此处无法使用 throws,只能用 try-catch
// 因为父类 Thread run 中没有 throws
// 默认是向上抛出异常,实际开发中推荐使用日记框架记录异常信息
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
// 创建实例,属于向上转型
Thread t = new myThread();
// 调用方法
// t.run();
t.start();
while (true){
System.out.println("Hello main.");
Thread.sleep(1000);
}
}
}
注意:使用 t.run(),并没有真正创建线程,只是直接调用了重写的 run():
run() 相当于 Thread 的入口,新的线程启动就会自动执行 run() 方法,因此 run 不需要手动调用。

调用 start() 才是真正创建了线程,且每个 Thread 对象都只能 start() 一次
start() 是 JVM 提供的方法,本质上是调用操作系统的 API。

观察输出结果,可见有时候是 main 在前,有时候却是 thread 在前。这是因为多个线程执行时,它们的调度顺序是随机的,谁先执行是无法预测的。
3.2 实现 Runnable,重写 run
此时的 Runnable runnable 相当于一个"可以执行的任务",只是一段逻辑,最终还是要通过 Thread 真正创建出线程。
线程要做的工作在 Runnable 中表示,而不是直接重写 Thread 中的 run,能够解耦合****(任务和 Thread 线程这个概念解耦),后续如果要修改代码,采用这种方法会更简便。
java
class myThread2 implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new myThread2();
Thread t = new Thread(runnable);
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
3.3 基于3.1,使用匿名内部类
上面两个方法中,我们新建的类都被赋予了名字:myThread 和 myThread2,而所谓的"匿名内部类"就是没有名字,直接在 {} 里面编写子类定义的代码,从而创建了这个匿名内部类的实例,并把实例的引用赋值给 t。
回顾------使用匿名内部类的前提:
- 必须有父类或接口;
- 只能继承一个类或实现一个接口;
- 必须实现所有的抽象方法;
- 仅能使用一次,无法再次创建该匿名类的实例。

3.4 基于3.2,使用匿名内部类
java
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread t = new Thread(r);
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
3.5 【推荐】使用 lambda 表达式
Lambda 本质上是一个匿名函数,只能用于实现函数式接口(即只有一个抽象方法的接口)。
最主要的用途是作为 "回调函数" 被调用。
java
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
📎回调函数
回调函数 本质上是一种 将代码作为参数传递给其他代码的编程模式。
简单说就是:你定义了一段逻辑,但不立即执行,而是把它"交给"某个方法或框架,由对方在合适的时机去调用它。
① 上面代码中的 Lambda 表达式就是一个回调函数:
定义行为:Lambda 中定义了"每隔一秒打印 hello thread "这个任务
传递行为:将这个任务作为参数传递给 Thread 的构造函数
触发执行:当调用 t.start() 时,线程会在合适的时机(启动后)自动执行这个回调
② 为什么叫"回调"?
因为代码的 执行控制权 发生了反转:
我们编写了 Lambda 中的逻辑,但没有直接调用它,而是把它交给了 Thread 对象,由Thread 在合适的时机回调这段代码。
③ 回调的执行流程:

3.6 使用 jconsole 命令观察线程
jconsole 在 JDK 安装路径下的 bin 包里面。

* 可以在 IDEA 的 File -> Project Structure 查看 JDK 的安装路径,从而找到 jconsole
如下,点击本地进程,选择 Demo1,连接之后选择"不安全的连接",再点击任务栏中的"连接"


线程被默认命名为 Thread-0,多个线程则由此递增:Thread-1,Thread-2...
除了 main 和 Thread-0 线程外,其他的都是 JVM 内置的线程,启动任何一个 Java 进程,都会自带这些线程,而它们的专有名词叫做 "后台线程"(下文提及)。
点击任何一条线程右边显示的信息中 "堆栈跟踪"下面的信息是指线程的调用栈 ,获取 线程状态(下文提及)的时刻,显示线程中的代码执行到哪里了。
3.7 Thread 常见构造方法
|--------------------------------------|--------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象传教线程对象,并命名 |
java
// 方法1
Thread t1 = new Thread();
// 方法2
Runnable runnable = new myThread();
Thread t2 = new Thread(runnable);
// Thread t2 = new Thread(new myThread());
给线程命名是为了方便程序员进行调试,命名时一般起具有代表性、有意义的名称:
java
// 方法3
Thread t3 = new Thread("线程1");
// 用 lambda 表达式时命名方法
Thread t3 = new Thread(() -> {
// ...
},"线程1");
// 方法4
Thread t4 = new Thread(new myThread(), "线程2");

示例:
java
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true){
System.out.println("hello t1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t1");
t1.start();
Thread t2 = new Thread(() -> {
while (true){
System.out.println("hello t2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t2");
t2.start();
Thread t3 = new Thread(() -> {
while (true){
System.out.println("hello t3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"t3");
t3.start();
for (int i = 0; i < 3; i++) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}

当我们运行上述代码,发现主线程 main 输出3次结果之后程序还在运行。而查看 jconsole 时,发现 mian 线程已销毁。按照以前单核的思路,main 方法结束之后,整个程序就结束了。但现在多核 CPU 下,主线程结束并不代表整个进程结束。
四、Thread 的常见属性
|------------|-----------------|
| 属性 | 获取方法 |
| ID,线程的唯一标识 | getId() |
| 名称 | getName() |
| 优先级 | getPriority |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
| 状态 | getState() |
4.1 获取当前线程的引用

因为 Lambda 的定义是在 new Thread 之前,即在 Thread t 声明之前,所以无法获取引用变量 t。正确的获取方法是使用 Thread.currentThread() 这个静态方法。哪个线程中调用,获取到的就是哪个线程的 Thread 引用。
4.2 后台线程
JVM 会在一个进程的所有非后台线程结束后,才会结束运行。
非后台线程也叫前台线程,像上文3.6中涉及到的 main 和 Thread-0 就是前台线程,只有它们两个结束,整个进程才会结束。也就是说,后台线程的结束并不会影响进程的结束,但进程结束它们也就随之结束。
判断是否是后台线程,使用isDaemon() 方法;设置成后台线程,使用 **setDaemon()**方法。
示例:
java
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 设置后台线程得在 start 之前
t.setDaemon(true);
t.start();
for (int i = 0; i < 3; i++) {
System.out.println("hello main");
Thread.sleep(1000);
}
System.out.println(t.isDaemon());
System.out.println("main 结束了");
}
}

4.3 是否存活
可以简单地理解为 run 方法是否结束。
复杂的理解:

当你在 Java 中创建一个 Thread 对象并调用 start() 时:
JVM 会向操作系统请求创建一个真实的系统线程;
Java 的 Thread 对象与该系统线程建立了1对1的绑定关系。
java
Thread t = new Thread(() -> {...}); // 1. 创建Java对象
t.start(); // 2. JVM 创建系统线程,建立映射
Thread 对象和系统线程两者的生命周期不同:

当执行完 run() 方法之后,系统线程就被销毁,而 Thread 对象还"存活"。
java
Thread t = new Thread(() -> {
System.out.println("线程运行中");
// 任务执行完毕,线程自然结束
});
t.start(); // 系统线程创建并运行
// 当 run() 方法执行完毕,系统线程被销毁
// 但是!Thread 对象 t 仍然存在
System.out.println(t.isAlive()); // false - 系统线程已死
// t 这个对象还在内存中,只是它绑定的系统线程已经不在了
示例:
java
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 在调用 start 之前,没有真正创建线程,所以这里的结果一定是 false
System.out.println(t.isAlive());
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(t.isAlive());
Thread.sleep(1000);
}
// t 对象仍然可以被访问
System.out.println("Thread 对象还在:"+t.getName());
}
}


· 根据 随机调度 的规则,打印的结果可能是 4 个 true,也可能是 3 个 true。
4.4 是否中断
"中断"的表达并不准确,而应该是判断是否"终止"。
如果未知 Java 提供的判断是否中断的方法,使用自定义变量来判断线程是否中断的代码如下:
java
public class Demo4 {
private static boolean isFinished = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread 结束");
});
t.start();
Thread.sleep(3000);
isFinished = true;
}
}
📎变量捕获
如果要将 isFinished 定义成局部变量,是否可行?

不可行,给出了警告:Lambda 表达式中的变量必须是 final 或 等同于 final。
为什么有这个限制?
局部变量是存储在栈中的,当main方法执行完毕(执行到第27行,但子线程thread还没结束),栈帧被销毁,局部变量就不存在了。但因为子线程可能还在运行,Lambda 表达式如果直接访问这个变量,就会出现"访问已销毁变量"的问题。
Java 通过 变量捕获 解决上面的问题:Lambda 会复制外部变量的值,而不是直接引用。
复制,意味着这样的变量不适合修改,因为即使修改了一方,另一方并不会随之变化(也就是说本质上是两个变量)。因此 Java 不允许修改:要么定义成 final,要么就不要做任何修改(也就是等同于 final)。

如果 Lambda 捕获的变量是引用类型呢?


Lambda 捕获局部变量时,遵循 值捕获 原则:
基本类型:捕获的是值本身,不能修改;
引用类型:捕获的是引用(地址),不能修改引用指向,但可以通过引用修改对象的内容。
但 isFinished 是成员变量时,就不再是"变量捕获"的语法,而切换到 "内部类访问外部类成员" 的语法。Lambda 表达式本质上是函数式接口,相当于一个内部类,内部类本来就能够访问外部类的成员。
而成员变量生命周期是由GC(垃圾回收)来管理的。在 Lambda 里面不担心变量生命周期失效的问题,也就不需要复制变量,从而不必限制用 final 修饰之类的问题。
前面都是铺垫内容,下面将讲解 Java 的 Thread 对象提供的用于判定线程是否中断的方法------isInterrupted(),以及设置中断的方法------ interrupt()
java
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break; // 关键
}
}
System.out.println("thread 结束");
});
t.start();
Thread.sleep(3000);
System.out.println("main 线程尝试阻止 t 线程......");
t.interrupt(); // 将t终止
}
}
上面的代码逻辑是一旦 t 线程被中断,捕捉到异常将会跳出循环结束线程。那么如果 catch 语句里面什么都不写呢?

我们发现 t 线程仍在运行。
正常情况下,调用 interrupt 方法就会修改 isInterrupted 方法内部的标志位,将其设置为 true;但是上述代码中的interrupt 将 sleep 唤醒了。这种被提前唤醒的情况下,sleep 就会在唤醒之后,将 isInterrupted 的标志位设置回 false。因此线程如果继续执行循环的条件判定,就会发现能够继续执行。
为此 catch 语句块应该如何编写,可以由程序员自行决定,可以让线程立即结束,还是等会结束,还是继续执行。
4.5 等待一个线程
多个线程之间并发执行时,根据随机调度原则,哪个线程先结束哪个后结束,程序员并不清楚。而 join() 方法能够设置一个线程等待其他线程结束之后,再进行它的工作。
使用 join 时,也需要抛出异常。
虽然也可以通过 sleep 设置休眠时间,来控制线程结束的先后顺序,但是有的情况下,这样的设定并不科学。
例如,希望 t 先结束,main 就紧跟着结束,如果通过设置休眠时间的方式,则并不靠谱:
java
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();
System.out.println("main 结束了");
}
}
上述代码执行到 t.join(); 时,main 线程就会"阻塞等待",一直等到 t 线程执行完毕,join 才继续执行。
我们会留意到上面 t 线程是被限制执行次数的,如果 t 进入了循环,主线程就会一直等下去,这种做法并不好。更科学的做法是设置好等待时间,一旦超过这个时间,join 就不等了:
java
t.join(3000);
t.join(3000,500);
单个参数单位是毫秒,设置两个参数则第一个参数单位是毫秒,第二个参数单位是纳秒。

一般不会设置到 纳秒 这个精确位,因为计算机中(尤其是应用程序中)很难进行 ns 级别的精准时间测量。针对精准的时间计算,有一类操作系统,即"实时操作系统"可以做到,比如应用于工业/航天/军事等领域的实时操作系统。
4.6 休眠线程
因为线程的调度是不可控的,所以 sleep 休眠方法只能保证 实际休眠时间是 ≥ 参数设置的休眠时间的。
代码调用 sleep,相当于要当前线程让出 CPU 资源,后续设置的时间到了的时候,需要操作系统内核,就把这个线程重新调到 CPU 上,才能继续执行。但设置时间到了,并不意味着立即被执行,而只是意味着允许被调度了。
sleep 方法的特殊写法:t.sleep(0),意味着令 t 放弃 CPU 资源,等待操作系统重新调度。也相当于将执行机会让给其他线程。
五、线程状态
线程的状态是一个枚举类型 Thread.State
java
public class getAllState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
NEW:线程对象已创建,但还没启动(start());
TERMINATED:线程执行完毕或因异常退出;
RUNNABLE:可执行,又分为 正在执行 和 等待操作系统资源(即包含了"就绪"和"运行"两种状态);
BLOCKED:线程在等待获取锁(synchronized)时被阻塞;
WAITING:线程无期限 等待另一个线程执行特定操作(wait()、join() 等);
TIMED_WAITING:线程等待指定时间 (sleep()、wait(timeout)、join(timeout) 等)。
除了可以使用 线程对象.getState() 方法查看线程状态,还可以在 jconsole 中查看。在 jconsole 中还能查看线程具体的调用栈,尤其是线程处于阻塞状态时,可以查看阻塞的位置。
在多线程的程序中,理解线程状态,是帮助程序员调试程序(找 bug)的关键。
六、总结
本文介绍了Java线程的基础知识,主要包括:
-
线程的产生背景;
-
Java线程与操作系统线程的关系;
-
创建线程的5种方法;
-
Thread的常见属性和方法;
-
线程控制方法,如 isAlive() 判断存活状态、interrupt() 中断线程、join() 等待线程结束等;
-
线程调度特性,包括随机性和 sleep() 方法的注意事项
7.线程的6种状态
8.重点补充知识:回调函数、变量捕获