4. 线程(TCB)
4.1 为什么有了进程还需要线程?
进程是资源分配的基本单位。创建和管理进程需要一定的系统开销。当客户端频繁向服务器发送请求时,如果每个任务都采用独立进程处理,就可能产生较大的资源开销。
-
为了降低开销,可以在一个进程中创建多个线程,让这些线程共享所属进程的资源,分别执行不同的任务。
-
进程: 主要负责资源的分配与管理。
-
线程: 使用所属进程的资源,负责执行具体任务,是 CPU 调度的基本单位。
-
线程(轻量级进程): 相比进程,线程不需要独立拥有一套完整的进程资源,因此创建和切换的开销通常较小。
注意: 进程结束后,操作系统会按照相应机制回收其资源。线程共享的是所属进程当前拥有的资源,并不是进程结束后继续使用遗留的内存。
4.2 进程与线程的关系
4.2.1 包含关系
- 一个操作系统上可以运行多个进程,每个进程又可以包含一个或多个线程。
- 一个进程可以包含多个线程。
- 进程与线程是包含关系。
- 每个进程都有自己的独立虚拟地址空间,同一进程中的多个线程共享该进程的地址空间及相应资源。
4.2.2 进程与线程的区别
| 对比内容 | 进程 | 线程 |
|---|---|---|
| 主要作用 | 管理和拥有资源 | 执行任务 |
| 基本单位 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 内存空间 | 拥有独立的虚拟地址空间 | 同一进程中的线程共享地址空间 |
| 资源开销 | 创建和管理开销通常较大 | 创建和管理开销通常较小 |
重点记忆:
- 进程是资源分配的基本单位。
- 线程是 CPU 调度的基本单位。
- 一个进程可以包含多个线程。
- 同一进程中的多个线程共享进程的资源。
4.2.3 线程与 PCB、TCB
- PCB(进程控制块): 用于记录和管理进程相关的信息。
- TCB(线程控制块): 用于记录和管理线程相关的信息。
入门阶段可以这样理解:
- 一个进程通常对应一个 PCB。
- 一个线程通常对应一个 TCB。
- 一个进程可以包含多个线程,因此一个进程可以关联多个 TCB。
4.2.4 进程与线程的内存关系
每个进程拥有独立的虚拟地址空间。同一个进程中的多个线程共享该进程的地址空间和部分资源。
因此:
- 不同进程之间通常不能直接访问彼此的普通内存。
- 同一进程中的线程可以访问共享数据。
- 一个线程修改共享数据,可能影响同一进程中的其他线程。
- 进程终止后,其资源通常会被操作系统回收。
4.2.5 线程安全问题
- 线程之间共享内存,因此多个线程同时操作同一份数据时,可能相互影响。
例如: - 多个线程同时修改同一个变量,如果没有适当的控制,就可能产生数据不一致的问题。(类似于不可重复读,前后两次读取到的数据不一致)
- 线程执行过程中如果出现未捕获的异常,通常会导致对应线程终止,但不意味着整个 JVM 一定终止。因此,需要妥善处理线程中的异常,并注意多个线程共享数据时可能产生的问题。
5. 创建线程的方式
Java 标准库提供了 Thread 类来执行线程,并提供了 Runnable 接口来描述线程需要执行的任务。
常见的创建线程的方式包括:
- 继承
Thread类。 - 实现
Runnable接口。 - 使用匿名内部类继承
Thread。 - 使用匿名内部类实现
Runnable。 - 使用 Lambda 表达式。
5.1 Thread 类
5.1.1 Thread 类的作用
Thread是 Java 标准库提供的线程类,可以通过创建Thread对象来表示一个线程。- 创建子类
MyThread子类继承Thread类并重写run()方法,可以定义线程需要执行的任务。
- 继承 Thread 类: 创建一个
Thread子类。 - 重写 run() 方法: 编写线程要执行的具体逻辑。
- 创建线程对象: 创建子类实例。
- 调用 start() 方法: 启动新线程,使其执行
run()方法中的任务。
5.1.2 run()与 start() 的区别
| 方法 | 作用 | |
|---|---|---|
run() | 定义线程需要执行的任务;直接调用时只是普通的方法调用 | |
start() | 启动新线程,使任务在新线程中执行 |
重点:
t.start(); // 启动新线程
t.run(); // 普通方法调用,不会启动新线程
5.1.3 sleep() 方法
Thread.sleep() 用于让当前正在执行的线程休眠指定时间,主动进入阻塞状态。
写在哪个线程内部就让哪个线程进入休眠状态。
例如:
Thread.sleep(1000); // 当前线程休眠 1000 毫秒
- 调用该方法时,需要处理
InterruptedException异常。 - 可以使用
try-catch捕获异常,也可以在允许的情况下通过throws声明异常。 - 需要注意:
Thread.sleep()是静态方法,调用它时休眠的是当前执行该语句的线程,而不是根据变量t指定休眠哪个线程。
5.1.4 主线程
- Java 程序执行
main()方法时,会运行主线程。 - 创建并启动其他线程后,主线程和新线程可以分别执行各自的任务。
- 多个线程的执行顺序不确定,由操作系统的线程调度机制等因素决定。
5.1.5 JVM 中的其他线程
- 程序运行时,除了自己创建的线程,例如主线程和通过
Thread创建的线程,JVM 还可能创建其他线程,用于垃圾回收,网络通信等工作。
5.2 Runnable 接口
5.2.1 Runnable 接口的作用
Runnable是 Java 提供的线程任务接口,其中定义了run()方法。- 通过实现
Runnable接口并重写run()方法,可以描述线程需要执行的任务 - 实现
Runnable接口本身不会直接启动线程,还需要将实现类了接口的实例对象作为参数传入Thread的构造方法,再调用start()。
5.2.2 通过Thread类 与 Runnable接口创建进程的区别
| 对比内容 | Thread 类 | Runnable 接口 |
|---|---|---|
| 实现方式 | 子类 继承Thread 类 | 子类实现Runnable 接口 |
| 任务编写位置 | 子类重写 run() 方法 | 子类实现接口中的 run() 方法 |
| 线程对象 | 子类实例本身就是线程对象 | 需要将实现了Runnable接口的子类实例化对象传给 Thread |
| 启动方式 | t.start() | t. start() |
5.2.3 两种方式的理解
第一种:继承 Thread 类
- 子类
MyThread重写run()方法,描述线程要执行的任务。 - 创建
MyThread实例对象后,可以将其赋值给Thread类型的变量(向上转型)。
Thread t = new MyThread();
- 再调用
start()启动线程。
第二种:实现 Runnable 接口
MyRunnable实现Runnable接口,重写run()方法,描述线程要执行的任务。- 然后创建
Thread实例,将MyRunnable实例作为参数传入。 - 最后调用
start()启动线程。
6. 创建线程的五种代码写法
6.1 继承 Thread 类
- 子类实现Thread类,重写run( )方法,方法中是新线程要执行的任务,实例化子类对象,调用 start( )方法执行 run()方法的语句。
- 这里的子类对象就是要执行的线程。
- 而实现Runnable接口的MyRunnable子类是要执行的实例化对象,需要把子类MyRunnable当作参数传给Thread
6.1.1 实现步骤
- 创建
MyThread子类并继承Thread。 - 重写
run()方法。 - 创建
MyThread实例对象。 - 调用
start()启动线程。
6.1.2 完整代码
package thread;
// 创建 Thread 的子类
class MyThread extends Thread {
// 重写 run 方法,定义新线程需要执行的任务
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
// 当前线程休眠 1000 毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
// 创建线程对象,此时只是完成准备工作
Thread t = new MyThread();//向上转型
// 启动新线程
t.start();
// 主线程执行自己的任务
while (true) {
System.out.println("hello world");
try {
//当前线程休眠 1000 毫秒
Thread.sleep(1000);
//判断哪个线程休眠,不要看 sleep()前面写了谁,而要看是哪一个线程正在执行这行代码。
//这里即使写成了t.sleep(1000);也是主线程休眠,因为这是在主线程里面的代码。
//Thread.sleep(1000);写在哪个线程的代码块中哪个线程休眠
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
6.1.3 重点理解
MyThread描述新线程需要执行的任务。t.start()启动新线程。main()方法中的循环由主线程执行。t.run()不会创建新线程。- 主线程和新线程的执行顺序不确定。
Thread.sleep(1000);写在哪个线程的代码块中哪个线程休眠
6.2 实现 Runnable 接口
6.2.1 实现步骤
- 创建
MyRunnable类并实现Runnable接口。 - 重写
run()方法。 - 创建
MyRunnable实例对象。 - 将实例对象传入
Thread构造方法。 - 调用
start()启动线程。
6.2.2 完整代码
package thread;
// 实现 Runnable 接口,描述线程要执行的任务
class MyRunnable 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) {
// 创建 Runnable 实现类的实例
MyRunnable runnable = new MyRunnable();
// 将任务对象传入 Thread
Thread t = new Thread(runnable);
// 启动新线程
t.start();
// 主线程执行自己的任务
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
6.2.3 重点理解
MyRunnable描述线程要执行的任务,是任务对象。Thread对象用于启动线程。new Thread(runnable)将任务对象传给线程对象。t.start()启动新线程。
6.3 使用匿名内部类继承 Thread
6.3.1 实现步骤
- 直接在
main()方法中创建Thread的匿名内部类对象,并重写run()方法。 - 上面6.1的简便写法都是一样的理解,只是添加了一个匿名内部类。
6.3.2 完整代码
package thread;
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
// 创建 Thread 的匿名内部类对象
Thread t = new Thread() {
@Override
public void run() {
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);
}
}
}
6.4 使用匿名内部类实现 Runnable
6.4.1 实现步骤
- 直接创建
Runnable的匿名内部类对象,重写run()方法,再将任务对象传给Thread。 - 上面6.2的简便写法都是一样的理解,只是添加了一个匿名内部类。
6.4.2 完整代码
package thread;
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
// 创建 Runnable 的匿名内部类对象
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
// 将 Runnable 任务对象传给 Thread
Thread t = new Thread(runnable);
// 启动新线程
t.start();
// 主线程执行自己的任务
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
6.5 使用 Lambda 表达式
6.5.1 实现步骤
- 使用 Lambda 表达式描述
Runnable任务,可以省略匿名内部类的部分代码。
6.5.2 完整代码
package thread;
public class Demo5 {
public static void main(String[] args) {
//新线程定义任务代码块
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");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
6.7五种线程创建方式的进程关系
6.7.1 五种方式是否运行在同一个进程?
是的,五种方式创建的新线程,都与
main()方法的主线程运行在同一个 Java 进程中。
五种创建线程的方式:
- 继承
Thread类(Demo1)。 - 实现
Runnable接口(Demo2)。 - 使用匿名内部类继承
Thread(Demo3)。 - 使用匿名内部类实现
Runnable(Demo4)。 - 使用 Lambda 表达式创建线程(Demo5)。
它们的区别在于如何定义线程执行的任务,而不是创建不同的进程。
6.7.2 五种方式的运行关系
Java 进程(JVM)
│
├── 主线程(main)
│ ├── 执行 main() 方法
│ ├── 输出 hello main / hello world
│ └── Thread.sleep(1000)
│
└── 新线程(Thread)
├── 执行 run() 方法中的任务
├── 输出 hello thread
└── Thread.sleep(1000)
6.7.3 五种方式的对比
| 代码 | 创建线程的方式 | 是否与主线程属于同一进程 |
|---|---|---|
| Demo1 | 继承 Thread 类 | 是 |
| Demo2 | 实现 Runnable 接口 | 是 |
| Demo3 | 匿名内部类继承 Thread | 是 |
| Demo4 | 匿名内部类实现 Runnable | 是 |
| Demo5 | Lambda 表达式 | 是 |
6.7.4 为什么属于同一个进程?
- 以 Demo5 为例:
Thread t = new Thread(() -> {
// 新线程执行的任务
System.out.println("hello thread");
});
t.start(); // 启动新线程
- 程序启动时,JVM 运行在 Java 进程中,主线程执行
main()方法。 - 调用
t.start()后,JVM 启动新线程执行任务,但不会因此启动一个新的 Java 进程。
因此:
main()方法由主线程执行。- Lambda 表达式中的任务由新线程执行。
- 两个线程属于同一个 Java 进程。
- 两个线程共享所属进程的地址空间及相应资源,但各自拥有独立的调用栈和执行状态。
6.7.5 本节重点总结
- 一个进程可以包含多个线程。
- Java 程序启动后,主线程执行
main()方法。 - 调用
Thread.start()可以启动新线程,但不会自动创建新的 Java 进程。 - 五种线程创建方式的区别在于任务的定义方式。
- 判断线程属于哪个进程,不能只看采用了哪种创建方式。
一句话记忆:Demo1~Demo5 都是在一个 Java 进程中,由主线程和新线程分别执行各自的任务。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2205_76017107/article/details/169169490




