线程

24 阅读8分钟

什么是线程

现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个 Java 程序,操作系统就会创建一个 Java 进程。现代操作系统调度的最小单元是线程,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。

线程的状态

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()方法 对该线程进行中断操作,interrupt() 方法代码如下:

public void interrupt() {
    // 如果是别的线程中断当前线程,会调用 checkAccess() 检查权限,
    // checkAccess() 可能会抛出 SecurityException 异常
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0(); // 只是设置中断标志位
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}

如果是别的线程中断当前线程,会调用 checkAccess() 检查权限。interrupt() 方法实际上只是给被中断的线程设置了一个中断标志,线程仍会继续运行。

这里的 this 是哪个线程? Thread.currentThread() 又是哪个线程呢?看下面的代码:

public class ThreadTest {

    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.test();
    }

    public static class MyThread extends Thread{

        @Override
        public void run() {

        }

        public void test() {
            System.out.println("this:" + this.getName());
            System.out.println("current Thread:" + Thread.currentThread().getName());
        }
    }
}

打印如下:

this:Thread-2
current Thread:main

由此可知,this 为 MyThread 线程,Thread.currentThread() 为主线程。

Thread 类中还有 2 个查询中断的方法很容易混淆:

  • isInterrupted()方法 的官方解释是:Tests whether this thread has been interrupted,意思是该方法用于检测 this thread 是否被中断了,这里 this Thread 就是上面的 MyThread。该方法不是静态方法,不清除标志。
  • interrupted()方法 的官方解释是:Tests whether the current thread has been interrupted,意思是该方法用于检测 current thread 是否被中断了,这里 current thread 就是上面的主线程,注意与上一个方法的区别,两者检测的线程对象是不一样的,并且 interrupted() 会清除当前线程的中断状态。该方法是一个静态方法。

下面看一个例子,我们在主线程中调用子线程的 interrupt() 方法:

public class ThreadInterrupt {

    public static void main(String[] args ) {
        MyThread thread = new MyThread();
        thread.start();

        // 调用子线程的 interrupt() 方法
        thread.interrupt();
        System.out.println("thread.isInterrupted():"+ thread.isInterrupted());
        System.out.println("thread is alive:"+ thread.isAlive());
    }

    public static class MyThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("thread is running: "+(i+1));
            }
        }
    }
}

打印如下:

thread is running: 1
thread is running: 2
thread is running: 3
...
thread is running: 19
thread is running: 20
thread is running: 21
thread.isInterrupted():true
thread is running: 22
thread is running: 23
...
thread is running: 55
thread is running: 56
thread is running: 57
thread is running: 58
thread is alive:true
thread is running: 59
thread is running: 60

从打印结果可以看出在主线程调用子线程的 interrupt() 方法后,子线程仍在继续执行,并未停止,并且 thread.isAlive() 返回值为 true,说明子线程仍然存活。但是 interrupt() 方法会给子线程设置中断标志为 true,所以 thread.isInterrupted() 方法返回值为 true。

在上面的 main() 方法中多加2条打印,获取 thread.interrupted() 的返回值,代码如下:

public class ThreadInterrupt {

    public static void main(String[] args ) {
        MyThread thread = new MyThread();
        thread.start();
        thread.interrupt();
        System.out.println("thread.isInterrupted():"+ thread.isInterrupted());
        // 新添加的2条打印
        System.out.println("first time call thread.interrupted():"+ thread.interrupted());
        System.out.println("second time call thread.interrupted():"+ thread.interrupted());
        System.out.println("thread is alive:"+ thread.isAlive());
    }

    public static class MyThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("thread is running: "+(i+1));
            }
        }
    }
}

打印如下:

thread.isInterrupted():true
first time call thread.interrupted():false
second time call thread.interrupted():false
thread is alive:true
thread is running: 1
thread is running: 2
thread is running: 3
...

看到输出结果你可能会有疑惑,既然 interrupted() 会清除当前线程的中断状态,那为什么两个 thread.interrupted() 方法返回的都是 false,而不是预期的一个 true 一个 false ?注意!这是一个坑!interrupted() 方法测试的是当前线程是否被中断,清除的也是当前线程的中断状态,这里当前线程是主线程,而 thread.interrupt() 中断的是子线程,两者作用的对象不是同一个。

把上面的例子改一下,改为中断主线程,代码如下:

public class ThreadInterrupt {

    public static void main(String[] args ) {
        MyThread thread = new MyThread();
        thread.start();
        // 中断主线程
        Thread.currentThread().interrupt();
        System.out.println("thread.isInterrupted():"+ Thread.currentThread().isInterrupted());
        System.out.println("first time call thread.interrupted():"+ thread.interrupted());
        System.out.println("second time call thread.interrupted():"+ thread.interrupted());
    }

    public static class MyThread extends Thread {

        @Override
        public void run() {
        }
    }
}

打印如下:

thread.isInterrupted():true
first time call thread.interrupted():true
second time call thread.interrupted():false

这样就与预期的打印结果一致了。

在 interrupt() 方法上还有一段描述:

If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.

什么意思呢?意思是如果这个线程阻塞在调用 Object类 的 wait(), wait(long), or wait(long, int) 这几个方法,或者阻塞在调用 Thread 类的 join(), join(long), join(long, int), sleep(long) 或者 sleep(long, int) 这几个方法时,调用这个线程的 interrupt() 方法会清除线程的中断状态,并且会抛出 InterruptedException 异常。看下面的代码:

public class ThreadInterrupt {

    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
        thread.interrupt();

        // 休眠主线程,让子线程的中断先执行
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        System.out.println("thread.isInterrupted():"+ thread.isInterrupted());
    }
    
    public static class MyThread extends Thread{

        @Override
        public void run() {
            while(true){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("thread is interrupted");
                }
            }
        }
    }
}

输出结果如下:

thread is interrupted
thread.isInterrupted():false

从打印结果可以看出,MyThread 线程的中断标识位被清除了,所以 thread.isInterrupted() 返回值为 false 。

安全地终止线程

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

public class Shutdown {
    public static void main(String[] args) throws Exception {
        Runner one = new Runner();
        Thread threadOne = new Thread(one, "threadOne");
        threadOne.start();
        // 睡眠 1 秒,使 threadOne 能够感知中断
        TimeUnit.SECONDS.sleep(1);
        threadOne.interrupt();

        Runner two = new Runner();
        Thread threadTwo = new Thread(two, "threadTwo");
        threadTwo.start();
        // 睡眠 1 秒
        TimeUnit.SECONDS.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {

        private long i;

        private volatile boolean flag = true;

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

        public void cancel() {
            flag = false;
        }
    }
}

输出结果如下:

Count i = 1717961046
Count i = 1764378781

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