线程 Thread

167 阅读15分钟

《并发概述》讲了,线程只不过是更“轻量级”的进程,支持快速进行任务切换,从而并发执行更高效,提升 CPU 利用率。

无论是进程还是线程,都不是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 用于创建一个新线程,并立即开始执行。

// 成功返回 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

void pthread_exit(void *retval);

pthread_join

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

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

pthread_self

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

pthread_t pthread_self(void);

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

//  
// 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);  
}

输出结果如下:

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的线程。

线程相关类

image.png

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

Runnable

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

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 调用了线程 Bjoin() 方法,线程 A 会进入阻塞状态,直到线程 B 结束执行。

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

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

sleepjoinsuspend ,这三个方法类似,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 调用。

  • 数数任务
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 次。

  • 多线程数数
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 次。

输出结果如下:

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

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

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

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() 方法已被弃用,不建议使用。

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

  • 带标志位的任务
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 变量)来控制线程的终止。

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

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 上。