【Java 多线程】5 - 2 多线程常用方法

2§5-2 多线程常用方法

5-2.1 Thread 提供的方法

Thread 提供了许多多线程常用的方法。

成员方法

方法 描述
String getName() 返回线程名称
void setName(String name) 设置线程名称
void start() 启动线程执行,由 JVM 执行线程的 run 方法

静态方法

静态方法 描述
Thread currentThread() 返回当前执行线程对象的引用
void sleep(long millis) 设置线程休眠时间(毫秒)

注意

  1. start 方法会让两条线程并发运行:一条是当前线程(由 start 方法返回),另一条则是执行 run 方法的线程;

  2. start 不可在一条线程上调用多次,即线程仅能够启动一次,二次启动线程会抛出异常 IllegalThreadStateException

  3. 线程 ID 在线程创建时生成,是一个正长整型。每个线程 ID 是唯一的,在线程生命周期内保持不变。线程结束时,其 ID 可能被重用;

  4. 若未指定线程名称,其默认值为 Thread-X,序号 X 默认从 0 开始;

  5. 线程名称可以通过 setName 方法设置,也可以通过构造器设置(Thread),若要在子类构造器中实现,子类构造器需要重载;

  6. JVM 启动时,会自动启动多条线程,其中一条线程就是 main 线程,其作用就是调用 main 方法,并执行其中的代码;

    在以前,所写的所有代码都是单线程运行在 main 线程中;

  7. 调用 sleep 方法的线程会休眠指定时间,时间到了之后,线程会自动唤醒,继续执行;

  8. Java 程序启动时,JVM 会启动一条名为 main 的线程,运行 main 方法;每一条线程都有一个自己的栈,称为线程栈;

5-2.2 线程优先级

与线程优先级有关的成员方法有:

成员方法 描述
int getPriority() 返回线程优先级
void setPriority(int newPriority) 设置线程优先级

在了解线程优先级前,先来了解线程的调度机制。线程调度机制有两种:

  • 抢占式调度:多条线程抢占 CPU 的执行权,任何一条线程的执行时机和执行时间都是随机的;
  • 非抢占式调度:多条线程按照顺序轮流由 CPU 执行,每条线程的执行时间差不多;

Java 采取抢占式调度的线程调度方式,通过线程优先级调整每条线程抢占到 CPU 的执行权的概率,线程的执行随机

Thread 类定义了三个与线程优先级相关的常量:

public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;

这三个常量分别定义了线程最小优先级、默认优先级、最大优先级。不同的优先级只表明线程抢占到 CPU 执行权的概率,更高的优先级并不能说明线程一定能够优先执行完毕。

5-2.3 线程的生命周期和状态

与线程状态有关的方法有:

方法 描述
Thread.State getState() 返回线程的状态

线程的状态定义在了 Thread 的内部枚举类中。

下图简要地说明了线程的生命周期:

image

实际上,Java 在 Thread 内部定义了一个枚举类 State,用于声明线程的状态。

该类中有六个枚举常量,定义了线程的六种状态,线程在任意时刻只能为这六种状态中的其中之一:

枚举常量 描述
NEW 使用 new 运算符创建的线程,尚未开始,代码尚未执行。
RUNNABLE 调用了 start 方法后的线程,线程可能正在执行,也可能并不正在执行。
BLOCKED 线程阻塞,等待内置对象锁(并非 java.util.concurrent.Lock)。在其他所有线程释放锁且调度器允许该线程持有锁时,线程会被取消阻塞。
WAITING 无限期等待另一个线程通知调度器某一个条件时,线程会进入等待状态
TIMED_WAITING 调用具有超时参数的方法,线程进入计时等待状态
TERMINATED 线程终止(死亡)

注意

  • RUNNABLE 状态:线程在执行时,并不是一直都处于执行状态,而是由操作系统提供调度服务,给予线程一定时间来运行。所有现代桌面和服务器操作系统采用的都是抢占式调度系统。抢占式调度系统会给每一条线程一个时间片执行其任务,当时间片耗尽时,操作系统会再次采用抢占式调度让其他线程有机会工作。选择线程时,操作系统会考虑现成的优先级;

    也有少部分设备(如手机)可能会采用合作式调度策略,这种情况下,当且仅当线程调用了 yield 方法、被阻塞或等待时才会失去对 CPU 的控制;

  • BLOCKEDWAITING 状态:当线程被阻塞或处于等待状态时,线程会暂时处于非活跃状态,不执行任何代码,消耗最小的资源。由线程调度器重新激活该线程;

    有关线程等待的内容会在[5 - 4 等待唤醒机制](5 - 4 等待唤醒机制.md)一节中说明。由于调用 Object.wait, Thread.join 方法、等待 LockCondition,线程会进入等待状态。实际上,阻塞和等待状态的区别并不是特别明显;

    有些方法具有超时参数。调用这些方法会使得线程进入 TIMED_WAITING 计时等待状态。该状态会一直持续,直至超时,或收到正确通知时退出该状态。具有超时参数的方法有 Thread.sleep,以及超时版本的 Object.wait, Thread.join, Lock.tryLockCondition.await

  • TERMINATED 状态:线程进入终止状态主要有两种原因。

    • run 方法正常退出,线程自然死亡;
    • 未捕获的异常终止了 run 方法,线程突然终止;

    考虑到可能存在第二种情况,为提高程序的健壮性,应当适当地处理可能抛出的异常。

5-2.4 礼让和插入线程

与礼让和插入线程有关的方法有:

方法 描述
void join()
void join(long millis)
void join(long millis, int nanos)
等待线程终止
static void yeild() 提示调度器出让当前线程对某个处理器的使用

注意

  • 每条线程都会抢占 CPU 的执行权,得到执行权的线程会让 CPU 执行该线程一段随机长度时间;

  • 调用 yeild 方法,会无视其可能继续执行的时间,出让线程对该处理器的执行权,让线程重新抢夺执行权;

  • 调用 yeild 方法会尽可能地使多条线程的执行时间平均化,但同一条线程仍有可能二次抢夺到执行权;

    平均化的前提是多条线程都有在出让线程;

  • join 方法用于将指定线程插入到当前线程前,优先执行完所插入线程后再继续执行当前线程;

    所插入的线程应当是已经启动的线程;

5-2.5 中断线程

与中断线程有关的方法有:

方法 描述
void interrupt() 中断该线程
static boolean interrupted() 测试当前线程是否已经被中断,会清除线程的中断状态
boolean isInterrupted() 测试该线程是否已经被中断

废弃的老旧方法:在最初的 Java 版本中,stop, suspend, resume 方法可用于终止/挂起/继续一条线程。但是,自 JDK 1.2 起,这些方法就被废弃了。因此,在当前版本中,除了这些方法,并不存在一个方法能够强制终止一条线程。在实际开发过程中也不推荐使用这些方法。

中断线程的使用方法:但某些情况下,我们希望线程能够终止其运行,这时就需要使用 interrupt 中断线程。这个方法并不会立刻中断线程,而是会将线程的中断状态(interrupted status,一个 boolean 标志位)设置为 true。每条线程都具有这样的一个标志位,线程应当不时检查该标志位以确定自己是否被请求中断。

// 检查线程是否被中断
while (!Thread.currentThread().isInterrupted() && ...) {
    ...
}

但是,若被中断的线程正处于阻塞或等待状态时,尝试中断则会抛出 InterruptedException 异常。这是一个受查异常,由于 run 方法并没有声明抛出任何异常,因此只能够通过 try-catch-finally 环绕处理。有些线程所执行的任务十分重要,这些线程在处理中断请求所抛出的异常时应当格外小心。但更常见的情况是,大部分线程被请求中断都是为了终止该线程。

// 捕获可能发生的 InterruptedException
Runnable r = () -> {
    try {
        while (!Thread.currentThread().isInterrupted() && ...) {
            ...
        }
    } catch (InterruptedException e) {
        // 异常处理
        ...
    } finally {
        // 善后工作
        ...
    }
}

如果在每一轮循环迭代后,都会调用一个 sleep 或其他可中断方法,那么调用 isInterrupted 的检查就不是很有必要,也不好用。因为该线程进入阻塞状态后,其他线程可能会通过该线程对象向它发出中断请求,被中断线程就会立刻收到一个 InterruptedException。在这种情况下(循环体中调用阻塞方法),直接捕获异常比检查中断状态更好:

// 循环体调用阻塞方法,直接捕获异常
Runnable r = () -> {
    try {
        while (...) {
            ...
            Thread.sleep(delay);
        }
    } catch (InterruptedException e) {
        // 异常处理
        ...
    } finally {
        // 善后工作
        ...
    }
}

中断异常处理:异常处理是程序中十分重要的一个部分。在 catch 语句块中,应当妥善处理可能抛出的异常,千万不要什么也不做。若实在不知道该怎么做,有两种选择:

  • catch 语句块中,调用 Thread.currentThread().interrupt() 以设置中断状态,使得调用者能够测试中断状态;

    void subTask() {
        ...
        try {
            Thread.sleep(delay);
        } catch (InterruptedExcepion e) {
            // 异常处理
            Thread.currentThread().interrupt();
        } finally {
            // 善后工作
            ...
        }
    }
    

    这个方法由 run 方法或其子方法调用。

  • 或者,在方法签名处附上 throws InterruptedException,去掉 try 语句块,让调用者(或,最终调用者为 run 方法)能够捕获该异常;

    void subTask() throws InterruptedException {
        ...
        Thread.sleep(delay);
        ...
    }
    

5-2.6 守护线程

与守护线程相关的方法有:

成员方法 描述
boolean isDaemon() 测试线程是否为守护线程
void setDaemon(boolean on) 将线程标记为守护线程或用户线程

注意

  • 当所有正在运行的线程都为守护线程时,JVM 会退出;
  • setDaemon 方法必须在线程启动前调用,否则抛出异常 IllegalThreadStateException
  • ontrue 时,标志着该线程为守护线程;

守护线程的特点:当所有非守护线程执行完毕而退出后,守护线程会陆续退出。

注意,守护线程在此时会陆续退出,而不是立刻退出。

应用场景:一个应用程序往往会在后台创建多条线程,但一定会有一条线程为主线程,与程序主界面有关。当这条主线程退出时,其他线程可没有继续执行下去的必要了,就可以将这些线程设置为守护线程。一旦主线程退出,这些守护线程就没有继续执行下去的必要,陆陆续续退出。

例如:一个聊天软件,一条线程负责聊天,一条负责文件传输。当聊天线程结束时,文件传输没有继续执行的必要了,则可设置为守护线程。

5-2.7 处理未捕获的异常

当线程由于未捕获的异常而终止运行时,虚拟机则会调用接口 UncaughtExceptionHandlerThread 的内部接口)处理这个异常,将终止线程与异常作为参数传递给该接口。

与未捕获异常处理器有关的方法:

静态方法 描述
Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() 返回线程由于未捕获的异常意外终止时调用的默认处理器
void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler ueh) 设置线程由于为捕获的异常意外终止时调用的默认处理器
实例方法 描述
Thread.uncaughtExceptionHandler getUncaughtExceptionHandler() 返回线程由于未捕获的异常意外终止时调用的处理器
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler ueh) 设置线程由于未捕获的异常意外终止时调用的处理器

UncaughtExcepionHandler 接口:这是一个函数式接口,有且仅有一个方法。

方法 描述
void uncaughtException(Thread t, Throwable e) 当所给的线程由于未捕获异常而终止时调用,该方法抛出的任何异常都会被虚拟机忽略

注意

  • 线程由于未捕获异常而终止时,虚拟机调用 Thread.getDefaultUncaughtExceptionHandler().uncaughtException(Thread, Throwable) 处理异常(若有自定义处理器,则 getUncaughtExceptionHandler().uncaughtException(Thread, Throwable));

  • 可以通过方法 setDefaultUncaughtExceptionHandlersetUncaughtExceptionHandler 为任意一条线程单独设置(默认)未捕获异常处理器。注意,传递的参数应当为该接口的实现类,替代的处理器可以使用日志 API 将未捕获的异常报告发送到一个日志文件中;

  • 若没有设置默认处理器,则默认处理器为 null。若没有为一条线程设置异常处理器,则处理器为线程的 ThreadGroup 对象;

    ThreadGroup 简介:表示一组线程的线程组,实现了 UncaughtExceptoinHandler 接口,提供了一种管理线程的方式。默认情况下,手动创建的所有线程都处于同一线程组,也允许手动创建新的线程组。由于在线程集合上操作能实现更好的特性,因此不推荐在程序中使用线程组。

    ThreadGroupuncaughtException 方法行为如下:

    • 若该线程组具有父级线程组,则将相同参数传入父级线程组的 uncaughtException 方法并调用之;
    • 否则,方法检查线程是否具有默认异常处理器。若有,则传入相同参数,调用该处理器处理异常;
    • 否则,包含线程名称(getName)、线程栈回溯的错误信息将会通过 ThrowableprintStackTrace 方法打印到标准错误流(System.err)中;
posted @ 2023-08-27 21:19  Zebt  阅读(33)  评论(0)    收藏  举报