掘金 后端 ( ) • 2024-04-01 19:03

什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。

线程的状态

Java线程在运行过程中会处于不同的状态,看下图: image.png 一共有6种状态,分别是:

  1. New:线程启动前的状态。
  2. Runnable:线程在Java虚拟机中正在执行的状态。
  3. Blocked:阻塞状态。表示线程阻塞于锁。
  4. Waiting:等待状态。当前线程需要等待其他线程做出一些特定的动作(通知或中断)。线程暂时不活动,并且不运行任何代码,这消耗最少的资源,直到线程调度器重新激活它。
  5. Timed Waiting:超时等待状态。和等待状态不同的是,它是可以在指定的时间自行返回的。
  6. Terminated:终止状态。表示当前线程已经执行完毕。导致线程终止有2种情况:第1种是run()方法执行完毕正常退出;第2种是因为没有捕获的异常终止了run()方法,导致线程进入终止状态。

线程的状态变化如下图所示:

image.png 从上图中可以看到,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

创建线程

创建线程有2种方式:

  1. 继承Thread类。
  2. 实现Runnable接口。

一般推荐用实现Runnable接口的方式,原因是,一个类应该在其需要加强或修改时才会被继承。

实现Callable接口,重写call()方法也可以创建线程,但其本质还是实现Runnable接口的方式,Callable接口定义如下:

public interface Callable<V> {
    V call() throws Exception;
}

可以看到call()方法是有返回值的,其与Runnable接口的区别如下:

  1. Callable中的call()方法有返回值,Runnable中的run()方法没有返回值;
  2. Callable中的call()方法可以抛出异常,Runnable的run()方法不能抛出异常;
  3. 运行Callable可以拿到Future对象,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下就可以使用Future来监视目标线程调用call()方法的情况。但调用Future的get()方法获取结果时,当前线程会阻塞,直到call()方法返回结果。

下面是一个使用Callable接口的示例:

public class TestCallable {

    public static class MyTestCallable implements Callable{

        @Override
        public String call() throws Exception {
            return "Hello World";
        }
    }

    public static void main(String[] args) {
        MyTestCallable myTestCallable = new MyTestCallable();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future future = executorService.submit(myTestCallable);
        try{
            System.out.println(future.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

线程的中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

这里创建了两个线程,SleepThread和BusyThread,前者不停地睡眠,后者一直运行,然后对这两个线程分别进行中断操作,观察二者的中断标识位。

public class SleepUtils {
    public static final void second(long seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
        }
    }
}

public class Interrupted {
    public static void main(String[] args) throws Exception {
        // sleepThread不停的尝试睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
        sleepThread.setDaemon(true);
        // busyThread不停的运行
        Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();
        // 休眠5秒,让sleepThread和busyThread充分运行
        TimeUnit.SECONDS.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        // 防止sleepThread和busyThread立刻退出
        SleepUtils.second(2);
    }
    static class SleepRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
                SleepUtils.second(10);
            }
        }
    }
    static class BusyRunner implements Runnable {
        @Override
        public void run() {
            while (true) {
            }
        }
    }
}

输出结果如下:

SleepThread interrupted is false
BusyThread interrupted is true

从结果可以看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除。

安全地终止线程

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程。看下面的例子:

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
        TimeUnit.SECONDS.sleep(1);
        countThread.interrupt();
        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }
    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()){
                i++;
            }
            System.out.println("Count i = " + i);
        }
        public void cancel() {
            on = false;
        }
    }
}

输出结果如下:

Count i = 1717961046
Count i = 1764378781

上例中在主线程中尝试使用中断操作和标志位来终止线程,这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。