别在啃厚书了!线程知识点全在这里
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,所有线程共享进程的资源(如内存、文件描述符等),但每个线程拥有独立的程序计数器、栈和寄存器组。⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码,main()⼀般被称为主线程(Main Thread)。
一.认识线程
1.1什么是线程
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,所有线程共享进程的资源(如内存、文件描述符等),但每个线程拥有独立的程序计数器、栈和寄存器组。
⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码,main()⼀般被称为主线程(Main Thread)。
1.2为啥要有线程(线程的作用与优势)
线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。线程的出现主要是为了提高程序的执行效率和资源利用率。
1.2.1提高程序并发性
线程允许程序在同一时间内执行多个任务。通过将任务分解为多个线程,可以充分利用多核处理器的计算能力。例如,一个网络服务器可以同时处理多个客户端请求,每个请求由一个独立的线程处理。
1.2.2资源共享与通信效率
同一进程内的多个线程共享进程的内存空间和资源,线程间的通信比进程间通信更高效。共享内存使得数据交换无需通过复杂的IPC机制,减少了系统开销。
1.2.3响应性与用户体验(javaEE进阶后面会讲到)
在图形用户界面(GUI)应用中,主线程负责界面渲染,而工作线程处理耗时操作(如文件读写或网络请求)。这种分工避免了界面冻结,提升了用户体验。
1.2.4资源开销较低(重点理解)
创建和销毁线程比进程更轻量级,就是更快。线程的上下文切换成本低于进程切换,因为线程共享相同的地址空间和系统资源。
1.2.5简化复杂任务设计
多线程模型可以将复杂任务分解为多个并行执行的子任务。例如,数据处理任务可以分割为多个线程并行处理,再合并结果,显著缩短总执行时间。
1.3线程与进程的区别
进程是操作系统资源分配的基本单位,拥有独立的地址空间、文件描述符、系统资源等PCB属性。每个进程运行在独立的内存空间中,相互隔离,一个进程崩溃通常不会影响其他进程。
线程是CPU调度的基本单位,属于进程的一部分,共享同一进程的资源(如内存、文件等)。一个进程可以包含多个线程,这些线程共享进程的地址空间和资源。
根据以上定义,就是说进程是包含线程的,一个进程至少有一个线程,就是主线程,并且一个进程中只有一个主线程。
值得注意的是进程与进程之间不共享内存空间,同一个进程的线程之间共享同一片内存空间。
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
1.3.1资源占用与开销
进程需要独立的内存空间和系统资源,创建和销毁进程的开销较大。上下文切换时,需要保存和恢复整个进程的状态(如内存映射、寄存器等),效率较低。
线程共享进程的资源,创建和销毁线程的开销较小。上下文切换时只需保存线程的寄存器状态,效率更高。但线程间共享资源可能导致竞争条件,需要同步机制(如锁)来避免冲突。
1.3.2稳定性与隔离性
进程具有更强的隔离性,一个进程崩溃不会直接影响其他进程。适合需要高稳定性的场景,如服务端程序。
线程共享进程资源,一个线程崩溃可能导致整个进程退出。调试多线程程序更复杂,需处理竞态条件和死锁问题。
也就是说一个进程出问题了,不会影响其他进程,所以说进程具有较强的隔离性,较稳定。
但是一个线程出问题,那么该线程所在的进程就可能出问题,导致整个进程崩溃,所以说线程隔离性较弱,不稳定。
1.3.3适用场景
进程适合需要高隔离性、独立运行的任务,例如浏览器中每个标签页作为独立进程运行,避免单个页面崩溃影响整个浏览器。
线程适合需要高效协作、频繁通信的任务,例如Web服务器用多线程处理并发请求,共享资源以减少开销。
二.创建线程
方法一.继承Thread类
//创建一个新的类,让这个类继承标准库的Thread类
class MyThread extends Thread{
//重写Thread类中的run方法
@Override
public void run(){
while(true){
System.out.println("hello thread");
//休息1000ms==1s
//使用sleep方法时会发生中断异常
try{
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//创建一个类实例
Thread t=new MyThread();
//启动线程
t.start();
//若执行下面代码,线程将不会开启,只会执行run方法
//t.run();
while(true){
System.out.println("hello main");
//sleep是静态方法,只能是Thread自己调用,对象不能调用
Thread.sleep(1000);
}
}
}
方法二.实现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) throws InterruptedException {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
while (true) {
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法三.匿名内部类创建Thread子类对象
public class Demo3 {
//创建匿名内部类来创建一个线程
public static void main(String[] args) throws InterruptedException{
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();
}
}
}
};
//记住,调用start才是真正创建线程
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法四.匿名内部类创建Runnable子类对象
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
// Runnable runnable=new Runnable() {
// @Override
// public void run() {
//
// }
// };
// Thread t=new Thread(runnable);
Thread t=new Thread(new Runnable() {
@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);
}
}
}
方法五.lambda表达式创建Runnable子类对象(最为常见)
public class Demo5 {
//用lammbaa表达式创建一个线程
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
//里面的内容相当于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);
}
}
}
使用lambda表达式创建多个线程
public class Demo6 {
//使用lambaa表达式创建多个线程
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while(true){
System.out.println("hello thread1");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
},"线程1");
Thread t2=new Thread(()->{
while(true){
System.out.println("hello thread2");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
},"线程2");
Thread t3=new Thread(()->{
while(true){
System.out.println("hello thread3");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
},"线程3");
t1.start();
t2.start();
t3.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
//接下来上面的四个语句会交替执行
}
}
三.Thread类及其方法
3.1Thread类常见的方法
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) |
使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target,String name) | 使用Runnable对象创建xian'cheng |
Thread t1=new Thread();
Thread t2=new Thread(new Runnable());
Thread t3=new Thread("这是我的名字);
Thread t4=new Thread(new Runnable(),"这是我的名字");
3.2Thread类常见的属性

3.3如何获取当前线程的引用
| 方法 | 说明 |
| public static Thread currentThread() | 返回当前线程对象引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
3.4如何休眠当前线程
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
//throws InterruptedException是因为sleep()方法可能会中断
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
3.5线程中断
public static void main(String[] args) {
Thread t = new Thread(() -> {
Thread cur = Thread.currentThread();
System.out.println("子线程启动,准备循环执行任务...");
// 核心:用 isInterrupted() 控制循环
while (!cur.isInterrupted()) {
System.out.println("hello thread");
try {
// 模拟长时间任务/阻塞操作
Thread.sleep(1000); // 改成1秒方便测试
} catch (InterruptedException e) {
// 1. 收到中断信号,打印日志
System.out.println("子线程收到中断信号,准备退出...");
// 2. 关键:恢复中断标志位(因为sleep被中断时会自动清除标志位)
cur.interrupt();
// 3. 释放资源、收尾工作(比如关闭文件、网络连接等)
System.out.println("正在释放资源...");
// 这里可以写你的清理逻辑
// 4. 跳出循环,优雅退出
//不需要break,因为此时循环条件已经为假了
}
}
// 循环结束,线程收尾
System.out.println("子线程已安全退出 ✅");
});
下面还有一种写法,不推荐
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(1000000);
}catch(InterruptedException e){
//throw new RuntimeException(e);
//0
e.printStackTrace();
//退出之前做一些释放资源工作
}
}
});
上面这个代码Thread.sleep()被中断时的特殊行为:
线程在sleep()休眠时,如果被调用interrupt()会发生两件事:
1.立即抛出异InterruptionException异常
2.z自动清除线程的中断标记(把原来的true改成false)
所以上面的代码的循环不会停止,会一直打印"hello thread"
3.6操作系统管理线程的两个关键队列
就绪队列(Ready Queue)
就绪队列是操作系统中用于管理准备执行但尚未获得CPU资源的进程或线程的队列。这些进程已经具备运行条件,只需等待CPU调度器分配时间片即可执行。就绪队列通常采用优先级调度、时间片轮转等算法决定下一个运行的进程。
进程进入就绪队列的条件包括:新创建的进程完成初始化、阻塞状态的进程被唤醒(如I/O操作完成)、时间片用完的进程被重新调度等。操作系统通过维护就绪队列实现多任务的高效切换。
阻塞队列(Blocked Queue)
阻塞队列用于管理因等待某事件(如I/O完成、信号量释放等)而暂时无法执行的进程。这些进程会主动放弃CPU资源,直到外部条件满足后被唤醒。阻塞队列中的进程不参与CPU调度,直到其等待的事件发生。
进程进入阻塞队列的典型场景包括:请求未被满足的资源(如文件读写)、主动休眠(sleep)、等待同步信号(如锁或信号量)等。一旦事件完成,进程会被移至就绪队列重新等待调度。
核心区别
- 状态依赖:就绪队列中的进程仅需CPU资源即可运行;阻塞队列中的进程需等待外部事件。
- 调度参与:就绪队列参与CPU调度决策;阻塞队列不参与调度。
- 转换方向:阻塞队列的进程被唤醒后进入就绪队列;就绪队列的进程可能因时间片耗尽或主动阻塞而离开。
例如,在Java的线程模型中,Runnable状态对应就绪队列,WAITING或BLOCKED状态对应阻塞队列。
结合这两个队列,解释一下sleep(0)和sleep()
sleep(0) 与 sleep() 的区别
sleep(0)
调用 sleep(0) 会主动让出当前线程的剩余时间片,但不会真正进入休眠状态。操作系统会立即重新调度该线程,可能继续执行当前线程(如果优先级最高),也可能切换到其他就绪线程。
适用于需要避免忙等待的场景,例如在自旋锁中短暂让出 CPU,就是说主动放弃CPU使用权。
sleep()
不带参数的 sleep() 通常会导致语法错误,因标准库中 sleep 需要明确的时间参数(如秒或毫秒)。例如 sleep(1) 会让线程休眠至少 1 秒。
休眠期间线程完全暂停,不参与 CPU 调度,直到指定时间结束或信号中断。
3.7等待一个线程-join()
public class Demo10 {
private static long result=0;
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
for (int i = 0; i <100 ; i++) {
result+=i;
}
System.out.println("t线程计算完毕");
});
t.start();
// 给 t 线程留有一定的执行时间.
// Thread.sleep(1);
// 使用 join 是更优的解法.
// join 等待 t 线程结束.
//由于线程的随机调度,可能for循环还没有走完就打印result
//使用join等待t线程执行完才打印result
t.join();
System.out.println(result);
}
}

四.线程的状态
4.1观察线程的所有状态
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
4.2下面的图会好理解一点
五.多线程带来的风险-线程安全问题(重点)
5.1观察线程不安全
public class Demo13 {
private static int count = 0;
//private static Object locker = new Object();
//private static Object locker2 = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建两个线程, 分别对同一个变量进行 5w 次的 ++ 操作.
// 最终主线程打印结果.
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
// synchronized (locker) {
count++;
//}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
//synchronized (locker) {
count++;
//}
}
});
//必须是多个线程共用同一把锁,互斥执行,一个一个来,才能保证线程安全
t1.start();
t2.start();
// 让主线程等待, 等待上述的两个线程结束
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
上面这个代码目的是求0到5000的整数依次两次相加,但结果一直都比正确结果要小。
这就是线程问题。
5.2线程安全问题:
一个进程的多个线程,共享同一份内存资源.如果两个线程都尝试修改同一个变量,就可能出现冲突,使代码出现Bug.
5.2.1为什么count++会出现问题
看似count++只进行了一步操作,但在CPU中每个操作都对应三个指令。
1.load:把内存的count加载到寄存器中
2.add:把寄存器里的数据+1
3.save:把寄存器里的数据写回数据
并且这三步操作不是原子操作!操作系统可能在任何一步打断线程,切换到另一个线程执行,就会导致冲突。
举个例子
| 时间点 | 线程t1在做什么 | 线程t2在做什么 | 内存里的count值 |
| 0 | 0(初始值) | ||
| 1 | load(count)->把内存里的0读到t1的寄存器里,寄存器值=0 | 0 | |
| 2 | CPU切到t2线程,t1线程被挂起 | load(count)->把内存里的0读到t2的寄存器里,寄存器值=0 | 0 |
| 3 | CPU切回t1 | 0 | |
| 4 | add->t1的寄存器值变成0+1=1 | 0 | |
| 5 | save->把t1线程寄存器里的1写回内存,内存里的count变为1 | 1 | |
| 6 | CPU切回t2 | 1 | |
| 7 | add->t2的寄存器里的值变成0+1=1(注意!他之前的load的值是0) | 1 | |
| 8 | save->把t2寄存器里的1写回内存,内存里的count还是1 |
1 |
t2在t1还没把save写回内存的时候,就已经load了count的值
所以t2后续的add和save,都是基于它自己读到的0在操作,而不是t1写回后的1
最终两个线程都把1写回内存,相当于count++被合并成一次,结果就是1,而不是预期的2;
5.3线程安全的5大根源
5.3.1操作系统线程调度是随机的
1.线程什么时候上CPU,什么时候被切下来,完全有操作系统调度决定,程序员无法控制
2.刚才遇到的count++被打断,结果被覆盖,就是因为调度器在load/add/save三步中间,把CPU切给了另一个线程
3.这是线程安全问题的“土壤”,后面的问题都是在这个基础上发生的
5.3.2多个线程同时修改同一个共享变量
| 场景 | 会不会出问题 | 原因 |
| 单线程修改一个变量 | 没问题 | 没有竞争,不会被打断 |
| 多个线程修改不同变量 | 没问题 | 变量不共享,互不干扰 |
| 多线程同时读取统一变量 | 没问题 | 读取不会改变变量的值,不会冲突 |
| 多线程同时修改同一变量 | 出问题 | 这是线程安全问题的必要条件 |
5.3.3修改操作不是原子的(count++分三步,不是原子性的)
1.count++不是一条CPU指令,而是load->add->save三步操作
2.解决方法:用锁(synchronized/Lock)或者原子类(AtomicInteger),把这三步变成"原子操作",中间不被打断
5.3.4内存可见性问题(线程看不到其他线程的修改)
1.每个线程都有自己的工作内存(寄存器/缓存),修改变量时,会先写到自己的缓存里,再异步刷新到主内存
2.这就导致:线程A修改里变量的值,但线程B看不到,读到还是自己缓存里的旧值
3.解决方法:用volatile关键字修饰变量,强制线程每次都从主内存读,修改后立刻写回主内存,保证所有线程都能看到最新值
5.3.5指令重排(CPU/编译器打乱代码执行顺序)
这是CPU/编译器为了优化性能,会打乱代码执行顺序,只要逻辑结果不变就行了
看代码
int a=1;
int b=2;
CPU可能会先执行b=2,再执行a=1
单线程下结果没问题,但多线程下可能会出现意外情况
1.线程A执行a=1和b=2,CPU重排之后,先写b=2
2.线程B看到b=2,以为a也已经被赋值了,结果读到的a还是默认值0
3.解决方法:volatile关键字也能禁止指令重排序,保证代码按你写的顺序执行
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)