线程 Thread

无论是进程还是线程,都不是Java语言所独有的概念,而是操作系统提供的编程接口,被众多编程语言所支持。

提到操作系统核心API就不得不提POSIX标准了,POSIX 全称是Portable Operating System Interface,便携式操作系统接口的意思,旨在为类Unix操作系统提供一致的API接口。

Windows系统也有自己的系统API。

POSIX标准就规范了进程、线程的接口定义,其中线程的接口定义叫Pthreads。C/C++就是使用POSIX线程(Pthreads)来创建和管理线程,Python中threading模块也是,JVM 就是使用 C/C++ 实现的,当然也是使用Pthreads 管理线程喽...。

所以,要了解Java线程,可以先对 Pthreads 建立一个基本的认识。

Pthreads

先看 Pthreads(POSIX Threads)关于线程创建的 API:

⨳ pthread_create

pthread_create 用于创建一个新线程,并立即开始执行。

 

c

代码解读

复制代码

// 成功返回 0,失败返回错误码。 int pthread_create(pthread_t *thread, // 指向线程标识符的指针,用于存储新创建线程的标识符。 const pthread_attr_t *attr, // 线程属性,传递 `NULL` 表示使用默认属性。 void *(*start_routine)(void *), //指向线程函数的指针,新线程会执行此函数。 void *arg); //传递给线程函数的参数。

线程创建后会有个ID,这个ID会存放在第一个参数*thread指向位置,这个ID在需要时(如线程同步、线程管理)可以和操作系统内核中的线程对象关联。

线程属性用于控制线程的行为,例如线程的栈大小、分离状态、调度策略(线程优先级)等,

线程主要的目的就是执行函数,第三个参数是函数,第四个参数是函数所需的参数,就不意外了。

⨳ pthread_exit

pthread_exit 可以终止调用该函数的线程,并可选择性地返回一个指针给 pthread_join

 

c

代码解读

复制代码

void pthread_exit(void *retval);

⨳ pthread_join

pthread_join 用于等待指定线程结束,回收线程资源。

 

c

代码解读

复制代码

int pthread_join(pthread_t thread, // 要等待的线程的标识符。 void **retval); // 用于存储线程的返回值(由 `pthread_exit` 返回)

⨳ pthread_self

pthread_self 可以获取当前线程的标识符。

 

c

代码解读

复制代码

pthread_t pthread_self(void);

了解了线程的创建与退出,就可以使用这个API管理线程了:

 

c

代码解读

复制代码

// // Created by CangoKing on 2024/8/31. // #include <pthread.h> #include <stdio.h> #define NUM_THREADS 5 // 线程执行的函数 void *print_message(void *threadid) { long tid; tid = (long)threadid; printf("Hello from thread #%ld\n", tid); pthread_exit(NULL); } int main() { // 声明了一个线程数组 threads,用于存储线程标识符。 pthread_t threads[NUM_THREADS]; int rc; long t; for (t = 0; t < NUM_THREADS; t++) { printf("In main: creating thread %ld\n", t); // pthread_create() 创建线程 rc = pthread_create(&threads[t], NULL, print_message, (void *)t); if (rc) { printf("Error: unable to create thread, %d\n", rc); exit(-1); } } // 等待所有线程完成 for (t = 0; t < NUM_THREADS; t++) { pthread_join(threads[t], NULL); } printf("All threads completed.\n"); pthread_exit(NULL); }

输出结果如下:

 

js

代码解读

复制代码

D:\WorkSpace\Agit\c_demo\thread_demo\main.exe In main: creating thread 0 In main: creating thread 1 In main: creating thread 2 In main: creating thread 3 In main: creating thread 4 Hello from thread #0 Hello from thread #1 Hello from thread #2 Hello from thread #3 Hello from thread #4 All threads completed.

回归正题,可以对比着,看一下Java的线程。

线程相关类

线程相关类很简单,一个代表线程任务的 Runnable 接口,一个线程基本类 Thread

Runnable

Runnable 接口定义了线程的基本行为,对应 Pthreads 创建线程API的中的传入的函数,实现 Runnable 接口的任务,也可以有成员属性,对应 Pthreads 创建线程API的中的传入的函数参数。

 

java

代码解读

复制代码

package java.lang; @FunctionalInterface public interface Runnable { /** * Runs this operation. */ void run(); }

可以看到,Runnable 接口也是一个函数式接口,可以使用Lambda表达式或方法引用快速实现。

之所以 Runnable 的 run 方法没有参数,是因为 Java 强调面向对象的设计模式,鼓励将线程逻辑与参数封装在对象中。

那 run 方法没有参数还可以理解,没有返回值就说不过去了,毕竟 Pthreads 还是可以通过 pthread_exit 方法传递返回值给等待的主线程,有返回值的前提条件是主线程要等待它执行完成,这一部分放到后面线程同步的时候再来讲解。

Thread

Thread类定义了Java语言层面的线程。其核心成员属性如下:

⨳ long tid:线程的ID

⨳ ThreadGroup group:线程所属的线程组, 线程组是一组具有相似行为的线程集合。

⨳ String name:线程的名字

线程组啦,线程的名字啦,这些 Pthreads规范都没有,毕竟线程的ID作为线程的唯一标识就够了,至于Java在语言层面上想怎么建立线程ID与其他标识的关系都没问题。

⨳ Runnable target:继承Runnable的对象实例,即该线程要执行的任务。

⨳ boolean daemon:是否为守护进程,默认为 false,即默认为用户线程。

凡存在用户线程执行,程序就不会停止运行,main 方法是用户线程,我们自己创建的线程,只要设置 daemon 为 false,也是用户线程,当所有用户线程停止,进程会停掉所有守护线程,退出程序。

JVM 中的 GC 回收线程就是守护线程,当所有的用户线程都执行完了,它也就没必要守护了,也会跟着销毁。

Pthreads规范也没有守护线程的概念,所有线程在创建后会持续存在,直到它们完成任务或被显式终止。

这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

 需要全套面试笔记的【点击此处即可】免费获取

Java 的 守护线程通常用于执行后台任务,如垃圾回收等,正因为Java 有守护线程,简化了线程开发,让用户不需要关注线线程的生命周期,也不需要在程执行完后进行资源回收,也就不需要设置Pthreads规范提到的分离状态了。

⨳ int priority:线程的优先级,最小优先级是1,最大优先级是10

理论上,线程的优先级越高,分配到CPU时间片的几率也就越高, 实际上,优先级只能作为一个参考数值,而且具体的线程优先级还和操作系统有关。

线程的优先级对应Pthreads规范的线程调度策略,但具体的优先级范围和行为取决于操作系统,可以设置但不一定有效。

线程生命周期

线程生命周期描述了线程从创建到终止过程中所经历的不同状态。

⨳ New(新建状态)

当线程对象被创建时,它处于新建状态。这时线程已经被创建,但尚未开始执行。对应 Thread 类的 new 方法,这只是 Java 语言层面的状态,在 Pthreads规范 找不到对应方法。

⨳ Runnable(可运行状态)

当线程的 start() 方法被调用时,线程进入可运行状态。这意味着线程已经准备好运行,并等待操作系统的线程调度器分配 CPU 时间片。

在可运行状态下,线程可能正在执行任务,也可能正在等待 CPU 时间片。对应 Pthreads规范的 pthread_create() 方法。

⨳ Blocked/Waiting/Timed Waiting(阻塞/等待/计时等待状态)

线程在某些情况下可能无法立即执行,会进入阻塞、等待或计时等待状态。这些状态下的线程无法获得 CPU 时间片,直到某个特定条件满足后才能继续运行。

  • Blocked(阻塞状态) : 当线程试图获取一个被其他线程持有的锁时,它会进入阻塞状态,直到该锁被释放。比如synchronized 块中的线程在等待锁时会进入阻塞状态。

  • Waiting(等待状态) : 线程在等待另一个线程执行特定操作(如通知或中断)时进入等待状态。比如线程调用 Object.wait() 方法,进入等待状态,直到被其他线程 notify() 或 notifyAll(),从而到切换到 Runnable状态。

  • Timed Waiting(计时等待状态) : 线程在指定时间内等待某个条件满足后继续执行。比如调用 Thread.sleep(long millis) 或 Object.wait(long timeout) 后,线程进入计时等待状态。

在操作系统层面,这三种状态就是休眠状态,会让出CPU的使用权。

⨳ Terminated(终止状态)

当线程完成了它的任务或者因异常终止时,线程进入终止状态。此时,线程已经不再是活动的,不能再次启动。对应 Pthreads规范的 pthread_exit() 方法。

在实际开发中,我们更多关注的是运行状态和休眠状态,特别是二者的相互转化。涉及方法如下:

⨳ void sleep(long millis):让正在执行的线程睡millis毫秒,哪个线程执行这个方法,哪个线程就让出CPU使用权,进入休眠状态。

⨳ void join():用于让当前线程等待另一个线程执行完成。举例来说,如果线程 A 调用了线程 B 的 join() 方法,线程 A 会进入阻塞状态,直到线程 B 结束执行。

⨳ void join(long millis):和join方法类似,但最多等待millis指定的时间

⨳ void suspend() :立即挂起目标线程,不会释放锁。

sleepjoin 和 suspend ,这三个方法类似,sleep 是要自己睡觉,会让出 CPU 使用权,join 是等待,等待某个线程先执行完成,也会让出 CPU 使用权,而 suspend 则是没有是一个没有终止时间的 sleep()

三个进入休眠的方法,虽然都不会释放锁,但至少sleepjoin都有个睡眠时间吧,而被 suspend 挂起的线程只能通过 resume() 方法唤醒,这如果永远不执行唤醒操作,那就是死锁了,风险很大,所以被废弃了。

Object 提供的 wait() 方法,也会让出 CPU 使用权进入休眠状态,但他会释放锁。

还有一个 yield 方法,是礼貌的谦让,客气一下,并不会让出 CPU 使用权进入休眠状态。

⨳ void yield():当一个线程调用 yield() 时,它会提示线程调度器当前线程愿意让出 CPU 使用权,允许其他具有相同优先级的线程或更高优先级的线程得到执行机会。但这只是一个提示,线程调度器可以选择忽略此提示,当前线程可能会继续运行。

线程的基本使用

创建线程

Thread 类提供了多种构造方法来创建线程,使得线程的创建过程能够根据需要进行灵活配置:

⨳ Thread():创建一个新的线程对象,线程的名称由系统自动分配,形式为 Thread- 加上一个递增的编号。例如,Thread-0Thread-1 等。

⨳ Thread(String name):指定 name 创建线程

⨳ Thread(Runnable target):指定 Runnable 的实例 target 创建线程

⨳ Thread(Runnable target, String name):指定 Runnable的实例 target 和 name 创建线程

⨳ Thread(ThreadGroup group, Runnable target, String name):指定线程组, Runnable的实例 和 name 创建线程

⨳ ...

这里使用最常规的方式,将任务封装成 Runnable 接口实现类,然后通过 Thread 调用。

  • 数数任务
 

java

代码解读

复制代码

package com.cango.thread; public class CountTask implements Runnable{ private int i; public CountTask(int i){ // 线程所需参数,可由用户自定义传入 this.i = i; } @Override public void run() { int count = 50; while (count>0) { i++; count--; System.out.println(Thread.currentThread().getName() + " " + i); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { } } } }

这个数数任务就是给成员变量 i 累加 50 次。

  • 多线程数数
 

java

代码解读

复制代码

public static void main(String[] args) { CountTask countTask = new CountTask(10); // 初始值为 10 Thread t1 = new Thread(countTask, "t1"); Thread t2 = new Thread(countTask, "t2"); t1.start(); t2.start(); }

创建两个线程,分别在 10 的基础上,向上数 50 次。

输出结果如下:

 

java

代码解读

复制代码

t2 12 t1 11 t2 13 t1 13 t1 14 t2 15 ... t2 105 t1 106 t2 107 t1 108 t2 109

根据输出结果可以看出虽然每个线程都将 i 累加了 50 次(局部变量 count 没有并发问题),但结果是 109,并不是 110,说明多线程同时修改成员变量有并发问题。

线程异常

Thread 内部有一个匿名函数接口 UncaughtExceptionHandler,我们可以用于处理线程执行过程中未被捕获的异常,即 run 方法内发生异常且没有使用 try-catch 块处理时,该异常将被传递到 UncaughtExceptionHandler

 

java

代码解读

复制代码

@FunctionalInterface public interface UncaughtExceptionHandler { void uncaughtException(Thread t, Throwable e); }

这个异常处理器可以为所有线程设置,也可以为某个特定的线程设置,在多线程编程中,这是非常有用的功能,尤其是在编写长时间运行的服务器或后台任务时。

 

java

代码解读

复制代码

CountTask countTask = new CountTask(10); Thread t1 = new Thread(countTask, "t1"); Thread t2 = new Thread(countTask, "t2"); t1.setUncaughtExceptionHandler((t, e) -> System.out.println(t.getName() + " thread exception: " + e.getMessage())); t1.start(); t2.start();

线程终止

在 Java 中,线程的终止有几种不同的方式,包括自然终止、异常终止和手动终止。

⨳ 自然终止:线程执行完它的 run() 方法后,正常结束执行并自动进入终止状态。这是线程最常见的终止方式。

⨳ 异常终止:线程在执行过程中由于未处理的异常而意外终止。当线程由于未捕获的异常而终止时,异常会传播到 run() 方法的边界,导致线程停止执行。此时,线程会进入 TERMINATED 状态,任何未完成的任务将被中断,且线程持有的任何资源都可能没有被正确释放。

⨳ 手动终止:指通过明确的指令或标志来请求一个线程停止运行。

自然终止是我们想要的,异常终止是程序写的有问题,那如果某个线程本身就是在不断循环执行任务,没有自然终止的可能,那该怎么手动终止这个线程呢?

Thread 类中确实存在一个 stop() 方法,该方法可以立即终止线程:

⨳ void stop():立即终止线程的执行,并释放线程所持有的所有锁。

但不推荐,stop() 很粗鲁,当线程持有锁并正在修改共享数据时,如果使用 stop() 强制终止线程,锁将被释放,但数据可能处于不一致状态。这会导致其他线程获得锁后访问到损坏的数据,从而引发逻辑错误。

而且如果这个线程正在持有资源(例如文件句柄、数据库连接等)时被 stop() 强制终止,资源可能没有机会被正确释放。这会导致资源泄漏,随着时间的推移,应用程序的可用资源可能被耗尽。

因此,从 Java 1.2 开始,stop() 方法已被弃用,不建议使用。

下面介绍一下通过标志位来,让线程自然的终止。

  • 带标志位的任务
 

java

代码解读

复制代码

package com.cango.thread; public class CountTaskWithFlag implements Runnable{ private int i; private volatile boolean running = true; public CountTaskWithFlag(int i){ this.i = i; } @Override public void run() { while (running) { i++; System.out.println(Thread.currentThread().getName() + " " + i); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { } } } public void stop(){ this.running = false; } }

原本的线程任务示例是根据局部变量 count 来计数,循环 50 遍就自动终止线程,现在使用标志位(即 volatile 变量)来控制线程的终止。

 

java

代码解读

复制代码

CountTaskWithFlag countTask = new CountTaskWithFlag(10); Thread t1 = new Thread(countTask, "t1"); t1.start(); Thread.sleep(3000); countTask.stop();// 终止线程 t1.join();

外部线程可以通过修改 running 来请求线程停止。这种方法允许线程安全地检查和响应终止请求。

其实 Thread 还提供了一个interrupt() 方法用于通知线程应当停止。

⨳ void interrupt():不会直接终止线程,而是设置线程的中断状态。

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true(可通过interrupted()查看),被设置中断标志的线程将继续正常运行,不受影响。
  • 如当前线程处于休眠状态,如线程调用了sleep,join,wait,condition.await,那么线程将立即退出被休眠状态,并抛出一个 InterruptedException 异常。

interrupt() 也是修改中断标志,比起我们自定义的中断标志位,它还可以中断休眠,让线程可以及时的进行终止。

 

java

代码解读

复制代码

package com.cango.thread; public class CountTaskWithInterrupt implements Runnable{ private int i; public CountTaskWithInterrupt(int i){ this.i = i; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { i++; System.out.println(Thread.currentThread().getName() + " " + i); try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重新设置中断状态 System.out.println("Thread was interrupted during sleep"); } } System.out.println("Thread is stopping"); } }

外部线程可以调用 thread.interrupt() 向 thread 发送中断信号,线程会通过 isInterrupted() 方法检测到中断并停止运行。

总结

本篇文章先介绍了POSIX标准的C语言实现,其中创建线程的核心方法就是pthread_create,了解到线程的目的就是执行线程函数。

然后了解了Java对线程的实现,封装线程函数和函数参数的 Runnable 接口和 封装线程属性的 Thread类。

进而分析了线程的生命周期,及其相关的一些进入休眠状态的方法,最后由介绍一下线程异常的处理方式,和优雅结束线程的方法。

是不是感觉也不怎么复杂,不过就是学习API调用而已,下一篇将讲解 Object 类提供的几个线程相关方法,可以思考一下为啥线程相关的方法会封装到作为所有类的父类 Object 上。

  • 17
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值