Thread

36 阅读9分钟

创建执行线程有两种方法:

  • 扩展 Thread 类。
  • 实现 Runnable 接口。
  • 使用 ExecutorService、Callable、Future 实现有返回结果的多线程

什么是Callable和Future?

Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable

Future 接口提供了方法来检查任务是否完成、获取任务的执行结果或等待任务完成。当使用 ExecutorServicesubmit() 方法提交一个 Callable 任务时,会返回一个 Future 对象,可以通过该对象来操作任务。

以下是一个简单示例,演示了如何使用 CallableFuture

javaCopy code
import java.util.concurrent.*;

public class CallableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Callable<Integer> callableTask = () -> {
            System.out.println("Callable task is running...");
            return 42;
        };

        Future<Integer> future = executorService.submit(callableTask);

        System.out.println("Submitted callable task");

        Integer result = future.get(); // 阻塞,直到任务完成并返回结果
        System.out.println("Result: " + result);

        executorService.shutdown();
    }
}

在上述示例中,Callable 接口表示一个返回整数结果的任务,通过 submit() 提交到线程池中执行。返回的 Future 对象可以用来获取任务的执行结果。需要注意的是,调用 future.get() 方法会阻塞,直到任务完成并返回结果。

通过使用 CallableFuture,可以方便地实现异步执行任务并获取任务结果的功能。

两种线程:

  • 守护线程。
  • 非守护线程(用户线程)。

Java程序结束执行过程的情形:

  • 程序执行Runtime类的exit()方法, 而且用户有权执行该方法。
  • 应用程序的所有非守护线程均已结束执行,无论是否有正在运行的守护线程。

守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。在线程start之前调用 isDaemon() 方法检查线程是否为守护线程,也可以使用 setDaemon() 方法将某个线程确立为守护线程。

在Java中有一个规定:当所有的非守护线程退出后,整个JVM进程就会退出。意思就是守护线程“不算作数”,守护线程不影响整个 JVM 进程的退出。

什么是Daemon线程?它有什么意义?

在 Java 中,线程可以分为两种主要类型:用户线程和守护(Daemon)线程。

用户线程(User Thread): 默认情况下,Java 线程创建的线程都是用户线程。用户线程不会影响 JVM 的退出,即使所有用户线程都已经结束,JVM 仍然会等待所有守护线程结束才会退出。

守护线程(Daemon Thread): 守护线程是一种在后台运行的线程,它的存在不会阻止 JVM 退出。当所有用户线程结束时,守护线程会被自动终止。守护线程通常用于执行一些后台任务,例如垃圾回收、内存监测等。

通过调用线程对象的 setDaemon(true) 方法,可以将线程设置为守护线程。一旦一个线程被设置为守护线程,它的所有子线程也会被自动设置为守护线程。

守护线程的主要意义在于它们可以用于在后台执行一些辅助性的任务,这些任务不需要阻止整个程序的退出。例如,JVM的垃圾回收线程就是Daemon线程,Finalizer也是守护线程。它在后台自动回收不再被引用的对象,释放内存资源。

需要注意的是,守护线程在执行过程中可能会被中断,因此不应该在它们的任务中执行需要完整性保证的操作。此外,守护线程通常用于执行一些周期性的、不需要稳定运行的任务。

轻量级阻塞与重量级阻塞

Thread.States类中定义线程的状态如下:

NEWThread对象已经创建,但是还没有开始执行。
RUNNABLEThread对象正在Java虚拟机中运行。
BLOCKED : Thread对象正在等待锁定。
WAITINGThread 对象正在等待另一个线程的动作。
TIME_WAITINGThread对象正在等待另一个线程的操作,但是有时间限制。
TERMINATEDThread对象已经完成了执行。
getState()方法获取Thread对象的状态,可以直接更改线程的状态。

能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING;
而像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是 BLOCKED。如图所示:调用不同的方法后,一个线程的状态迁移过程。

4b52e72afb4a09a94c6bec4f0099ffc.jpg 初始线程处于NEW状态,调用start()开始执行后,进入RUNNING或者READY状态。如果没有调用

任何的阻塞函数,线程只会在RUNNING和READY之间切换,也就是系统的时间片调度。这两种状态的

切换是操作系统完成的,除非手动调用yield()函数,放弃对CPU的占用。

一旦调用了图中的任何阻塞函数,线程就会进入WAITING或者TIMED_WAITING状态,两者的区别

只是前者为无限期阻塞,后者则传入了一个时间参数,阻塞一个有限的时间。如果使用了synchronized

关键字或者synchronized块,则会进入BLOCKED状态。

不太常见的阻塞/唤醒函数,LockSupport.park()/unpark()。这对函数非常关键,Concurrent包中

Lock的实现即依赖这一对操作原语。

因此thread.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。

Thread类和Runnable 接口

Runnable接口只定义了一种方法:run()方法。这是每个线程的主方法。当执行start()方法启动新线程时,它将调用run()方法。

Thread类其他常用方法:

  1. 获取和设置Thread对象信息的方法。
    • getId():该方法返回Thread对象的标识符。该标识符是在钱程创建时分配的一个正 整数。在线程的整个生命周期中是唯一且无法改变的。
    • getName()/setName():这两种方法允许你获取或设置Thread对象的名称。这个名 称是一个String对象,也可以在Thread类的构造函数中建立。
    • getPriority()/setPriority():你可以使用这两种方法来获取或设置Thread对象的优先 级。
    • isDaemon()/setDaemon():这两种方法允许你获取或建立Thread对象的守护条件。
    • getState():该方法返回Thread对象的状态。
  2. interrupt():中断目标线程,给目标线程发送一个中断信号,线程被打上中断标记。
  3. interrupted():判断目标线程是否被中断,但是将清除线程的中断标记。
  4. isinterrupted():判断目标线程是否被中断,不会清除中断标记。
  5. sleep(long ms):该方法将线程的执行暂停ms时间。
  6. join():暂停线程的执行,直到调用该方法的线程执行结束为止。可以使用该方法等待另一个Thread对象结束。
  7. setUncaughtExceptionHandler():当线程执行出现未校验异常时,该方法用于建立未校验异常的控制器。
  8. currentThread():Thread类的静态方法,返回实际执行该代码的Thread对象。

InterruptedException与interrupt()方法

Interrupted异常,什么情况下会抛出Interrupted异常 只有那些声明了会抛出InterruptedException的函数才会抛出异常,也就是下面这些常用的函数:

public static native void sleep(long millis) throws InterruptedException
{...}

public final void wait() throws InterruptedException {...}
public final void join() throws InterruptedException {...}

thread.isInterrupted()与Thread.interrupted()的区别

因为 thread.interrupted()相当于给线程发送了一个唤醒的信号,所以如果线程此时恰好处于WAITING或者TIMED_WAITING状态,就会抛出一个InterruptedException,并且线程被唤醒。而如果线程此时并没有被阻塞,则线程什么都不会做。但在后续,线程可以判断自己是否收到过其他线程发来的中断信号,然后做一些对应的处理。

这两个方法都是线程用来判断自己是否收到过中断信号的,前者是实例方法,后者是静态方法。二者的区别在于,前者只是读取中断状态,不修改状态;后者不仅读取中断状态,还会重置中断标志位。

举例说明

假设有一个厨师正在做饭,这个过程可以类比为一个线程在运行。现在,让我们使用这个例子来说明 "isInterrupted" 和 "interrupted" 方法之间的区别。

  1. isInterrupted() 方法:

假设你是一个餐厅的老板,你想让厨师在做饭时保持警觉,以便在有需要时能够停止做饭。你会在一个标志位上贴上一个小纸条,上面写着 "需要停止做饭"。厨师会时不时地看一眼这个纸条,以检查是否需要停止。在这个例子中,纸条就是线程的 "中断标志位"。

Thread chefThread = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 继续做饭...
        System.out.println("继续做饭...");
    }
    System.out.println("收到停止做饭指令,准备收拾厨房。");
});
chefThread.start();

// 一段时间后,需要停止做饭
chefThread.interrupt(); // 设置中断标志位

在这个例子中,厨师(线程)使用了 "isInterrupted()" 方法来检查中断标志位,以判断是否需要停止做饭。如果标志位被设置为中断,厨师就会停止做饭。

  1. interrupted() 方法示例:

现在,让我们看看另一个情况,小孩正在看动画片,父母通过敲门提醒他前来吃晚餐。小孩听到敲门声后会停止看动画片,然后去吃晚餐。

Thread childThread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // 继续看动画片...
        System.out.println("继续看动画片...");
    }
    System.out.println("收到停止看动画片指令,准备前往吃晚餐。");
});
childThread.start();

// 一段时间后,需要停止看动画片
childThread.interrupt(); // 设置中断标志位

运行结果可能如下所示:

继续看动画片...
继续看动画片...
继续看动画片...
收到停止看动画片指令,准备前往吃晚餐。

在这个例子中,小孩(线程)通过 "interrupted()" 方法来检查中断标志位。当小孩听到敲门声(收到中断请求)后,他会停止看动画片,然后去吃晚餐。需要注意的是,"interrupted()" 方法会在检查中断状态后清除中断状态,就像敲门声一旦听到后就不再重复响起。