老生常谈线程基础的几个问题

1,852 阅读9分钟

实现线程只有一种方式

我们知道启动线程至少可以通过以下四种方式:

  1. 实现 Runnable 接口
  2. 继承 Thread 类
  3. 线程池创建线程
  4. 带返回值的 Callable 创建线程

但是看它们的底层就一种方式,就是通过new Thread()实现,其他的只不过在它的上面做了层封装。

实现Runnable接口要比继承Thread类的更好

  1. 结构上分工更明确,线程本身属性和任务逻辑解耦。
  2. 某些情况下性能更好,直接把任务交给线程池执行,无需再次new Thread()。
  3. 可拓展性更好:实现接口可以多个,而继承只能单继承。

有的时候可能会问到启动线程为什么是start()方法,而不是run()方法,这个问题很简单,执行run()方法其实就是在执行一个类的普通方法,并没有启动一个线程,而start()方法点进去看是一个native方法。

image.png

当我们在执行 java 中的 start() 方法的时候,它的底层会调 JVM 由 c++ 编写的代码 Thread::start,然后c++代码再调操作系统的 create_thread 创建线程,创建完线程以后并不会马上运行,要等待CPU的调度。CPU的调度算法有很多,比如先来先服务调度算法(FIFO),最短优先(就是对短作业的优先调度)、时间片轮转调度等。如下图所示:

image.png

线程的状态

在Java中线程的生命周期中一共有 6 种状态。

  • NEW:初始状态,线程被构建,但是还没有调用start方法
  • RUNNABLE:运行状态,JAVA线程把操作系统中的就绪和运行两种状态统一称为运行中
  • BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了CPU使用权
  • WAITING: 等待状态
  • TIMED_WAITING:超时等待状态,超时以后自动返回
  • TERMINATED:终止状态,表示当前线程执行完毕

当然这也不是我说的,源码中就是这么定义的:

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
    TERMINATED;
}

下面是这六种状态的转换: image.png

New新创建

New 表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始调用 start() 方法,那么此时它的状态就是 New。而一旦线程调用了 start(),它的状态就会从 New 变成 Runnable。

Runnable运行状态

Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running和Ready ,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。

如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。

在Java中Blocked、Waiting、Timed Waiting,这三种状态统称为阻塞状态,下面分别来看下。

Blocked

从上图可以看出,从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁,jvm会把当前的线程放入到锁池中。当处于Blocked 的线程抢到 monitor 锁,就会从 Blocked 状态回到Runnable 状态。

Waiting 状态

我们看上图,线程进入 Waiting 状态有三种可能。

  1. 没有设置 Timeout 参数的 Object.wait() 方法,jvm会把当前线程放入到等待队列
  2. 没有设置 Timeout 参数的 Thread.join() 方法。
  3. LockSupport.park() 方法。

LockSupport.park()就是上文讲《ReentrantLock介绍及AQS源码精讲》里用到的。 Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。

当执行了LockSupport.unpark(),或者 join 的线程运行结束,或者被中断时可以进入 Runnable 状态。当调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,因为唤醒 Waiting 状态的线程能够调用 notify() 或 notifyAll(),肯定是已经持有了 monitor 锁,这时候处于 Waiting 状态的线程没有拿到monitor锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态。

Timed Waiting状态

在 Waiting 上面是 Timed Waiting 状态,这两个状态是非常相似的,区别仅在于有没有时间限制,Timed Waiting 会等待超时,由系统自动唤醒,或者在超时前被唤醒信号唤醒。

以下情况会让线程进入 Timed Waiting 状态。

  1. 设置了时间参数的 Thread.sleep(long millis) 方法。
  2. 设置了时间参数的 Object.wait(long timeout) 方法。
  3. 设置了时间参数的 Thread.join(long millis) 方法。
  4. 设置了时间参数的 LockSupport.parkNanos(long nanos)。
  5. LockSupport.parkUntil(long deadline) 方法。

在 Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态。当然,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark(),会直接恢复到 Runnable 状态,而无需经历 Blocked 状态。

Terminated 终止

Terminated 终止状态,要想进入这个状态有两种可能。

  1. run() 方法执行完毕,线程正常退出。
  2. 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

线程的停止interrupt

我们知道Thread提供了线程的一些操作方法,比如stop(),suspend() 和 resume(),这些方法已经被 Java 直接标记为 @Deprecated,这就说明这些方法是不建议大家使用的。

因为 stop() 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题。这种行为类似于在linux系统中执行 kill -9类似,它是一种不安全的操作。

而对于 suspend() 和 resume() 而言,它们的问题在于如果线程调用 suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被 resume() 之前,是不会被释放的。

interrupt

最正确的停止线程的方式是使用 interrupt,但 interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。

下面我们来看下例子:

public class InterruptExample implements Runnable {

    //interrupt相当于定义一个volatile的变量
    //volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InterruptExample());
        t1.start();
        Thread.sleep(5);
        //Main线程来决定t1线程的停止,发送一个中断信号, 中断标记变为true
        t1.interrupt();

    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println(Thread.currentThread().getName() + "--");
        }
    }
}

执行一下,运行了一会就停止了 image.png 主线程在调用t1的 interrupt() 之后,这个线程的中断标记位就会被设置成 true。每个线程都有这样的标记位,当线程执行时,会定期检查这个标记位,如果标记位被设置成 true,就说明有程序想终止该线程。在 while 循环体判断语句中,通过 Thread.currentThread().isInterrupt() 判断线程是否被中断,如果被置为true了,则跳出循环,线程就结束了,这个就是interrupt的简单用法。

阻塞状态下的线程中断

下面来看第二个例子,在循环中加了 Thread.sleep 1000秒。

public class InterruptSleepExample implements Runnable {

    //interrupt相当于定义一个volatile的变量
    //volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InterruptSleepExample());
        t1.start();
        Thread.sleep(5);
        //Main线程来决定t1线程的停止,发送一个中断信号, 中断标记变为true
        t1.interrupt();

    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                Thread.sleep(1000000);
            } catch (InterruptedException e) {//中断标记变为false
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "--");
        }

    }
}

再来看下运行结果,卡主了,并没有停止。这是因为main线程调用了 t1.interrupt(),此时 t1 正在 sleep中,这时候是接收不到中断信号的,要sleep结束以后才能收到。这样的中断太不及时了,我让你中断了,你缺还在傻傻的sleep中。

image.png Java开发的设计者已经考虑到了这一点,sleep、wait等方法可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。

这时候有几种做法:

  1. 直接捕获异常,不做处理,e.printStackTrace();打印下信息
  2. 将异常往外抛出,即在方法上 throws InterruptedException
  3. 再次中断,代码如下,加上 Thread.currentThread().interrupt();
@Override
public void run() {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {//中断标记变为false
            e.printStackTrace();
            //把中断标记修改为true
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + "--");
    }
}

这时候线程感受到了,我们人为的再把中断标记修改为true,线程就能停止了。一般情况下我们操作线程很少会用到interrupt,因为大多数情况下我们用的是线程池,线程池已经帮我封装好了,但是这方面的知识还是需要掌握的。 感谢收看,多多点赞~