无论是进程还是线程,都不是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
可以获取当前线程的标识符。
这边整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可】免费获取
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 的 守护线程通常用于执行后台任务,如垃圾回收等,正因为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()
:立即挂起目标线程,不会释放锁。
sleep
、join
和 suspend
,这三个方法类似,sleep
是要自己睡觉,会让出 CPU 使用权,join
是等待,等待某个线程先执行完成,也会让出 CPU 使用权,而 suspend
则是没有是一个没有终止时间的 sleep()
。
三个进入休眠的方法,虽然都不会释放锁,但至少sleep
和join
都有个睡眠时间吧,而被 suspend
挂起的线程只能通过 resume()
方法唤醒,这如果永远不执行唤醒操作,那就是死锁了,风险很大,所以被废弃了。
Object 提供的
wait()
方法,也会让出 CPU 使用权进入休眠状态,但他会释放锁。
还有一个 yield
方法,是礼貌的谦让,客气一下,并不会让出 CPU 使用权进入休眠状态。
⨳ void yield()
:当一个线程调用 yield()
时,它会提示线程调度器当前线程愿意让出 CPU 使用权,允许其他具有相同优先级的线程或更高优先级的线程得到执行机会。但这只是一个提示,线程调度器可以选择忽略此提示,当前线程可能会继续运行。
线程的基本使用
创建线程
Thread 类提供了多种构造方法来创建线程,使得线程的创建过程能够根据需要进行灵活配置:
⨳ Thread()
:创建一个新的线程对象,线程的名称由系统自动分配,形式为 Thread-
加上一个递增的编号。例如,Thread-0
、Thread-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
上。