Java 源码刨析 - 线程的状态有哪些?它是如何工作的?

线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在

一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。

   

线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态:

NEW新建状态,线程被创建出来,但尚未启动时的线程状态;

RUNNABLE就绪状态,表示可以运行的线程状态,它可能正在运行,或者是在排队等待操作系统给它分配 CPU 资源;

BLOCKED阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行 synchronized 代码块或者使用 synchronized 标记的方法;

WAITING等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了 Object.wait() 方法,那它就在等待另一个线程调用 Object.notify() Object.notifyAll() 方法;

TIMED_WAITING计时等待状态,和等待状态(WAITING)类似,它只是多了超时时间,比如调用了有超时时间设置的方法 Object.wait(long timeout) Thread.join(long timeout) 等这些方法时,它才会进入此状态;

TERMINATED终止状态,表示线程已经执行完成。

线程状态的源代码如下:

public enum State {

    /**

     * 新建状态,线程被创建出来,但尚未启动时的线程状态

     */

    NEW,

   

    /**

     * 就绪状态,表示可以运行的线程状态,但它在排队等待来自操作系统的 CPU 资源

     */

    RUNNABLE,

   

    /**

     * 阻塞等待锁的线程状态,表示正在处于阻塞状态的线程

     * 正在等待监视器锁,比如等待执行 synchronized 代码块或者

     * 使用 synchronized 标记的方法

     */

    BLOCKED,

   

    /**

     * 等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作。

     * 例如,一个线程调用了 Object.wait() 它在等待另一个线程调用

     * Object.notify() 或 Object.notifyAll()

     */

    WAITING,

   

    /**

     * 计时等待状态,和等待状态 (WAITING) 类似,只是多了超时时间,比如

     * 调用了有超时时间设置的方法 Object.wait(long timeout) 和 

     * Thread.join(long timeout) 就会进入此状态

     */

    TIMED_WAITING,

   

    /**

     * 终止状态,表示线程已经执行完成

     */

}

   

线程的工作模式是,首先先要创建线程并指定线程需要执行的业务方法,然后再调用线程的 start() 方法,此时线程就从 NEW(新建)状态变成了 RUNNABLE(就绪)状态;

然后线程会判断要执行的方法中有没有 synchronized 同步代码块,如果有并且其他线程也在使用此锁,那么线程就会变为 BLOCKED(阻塞等待)状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。

   

当遇到 Object.wait() Thread.join() 方法时,线程会变为 WAITING(等待状态)状态;

如果是带了超时时间的等待方法,那么线程会进入 TIMED_WAITING(计时等待)状态;

当有其他线程执行了 notify() notifyAll() 方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了,执行流程如下图所示:

BLOCKED WAITING 的区别】

虽然 BLOCKED WAITING 都有等待的含义,但二者有着本质的区别。

首先它们状态形成的调用方法不同

其次 BLOCKED 可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源

WAITING 则是因为自身调用 Object.wait() 或着是 Thread.join() 又或者是 LockSupport.park() 而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒。

比如当线程因为调用了 Object.wait() 而进入 WAITING 状态之后,则需要等待另一个线程执行 Object.notify() Object.notifyAll() 才能被唤醒。

   

start() run() 的区别】

首先从 Thread 源码来看,start() 方法属于 Thread 自身的方法,并且使用了 synchronized 来保证线程安全,源码如下:

public synchronized void start() {

    // 状态验证,不等于 NEW 的状态会抛出异常

    if (threadStatus != 0)

        throw new IllegalThreadStateException();

    // 通知线程组,此线程即将启动

   

    group.add(this);

    boolean started = false;

    try {

        start0();

        started = true;

    } finally {

        try {

            if (!started) {

                group.threadStartFailed(this);

            }

        } catch (Throwable ignore) {

            // 不处理任何异常,如果 start0 抛出异常,则它将被传递到调用堆栈上

        }

    }

}

   

run() 方法为 Runnable 的抽象方法,必须由调用类重写此方法,重写的 run() 方法其实就是此线程要执行的业务方法,源码如下:

public class Thread implements Runnable {

 // 忽略其他方法......

  private Runnable target;

  @Override

  public void run() {

      if (target != null) {

          target.run();

      }

  }

}

@FunctionalInterface

public interface Runnable {

    public abstract void run();

}

   

从执行的效果来说,start() 方法可以开启多线程,让线程从 NEW 状态转换成 RUNNABLE 状态,而 run() 方法只是一个普通的方法。

   

其次,它们可调用的次数不同,start() 方法不能被多次调用,否则会抛出 java.lang.IllegalStateException;而 run() 方法可以进行多次调用,因为它只是一个普通的方法而已。

   

【线程优先级】

Thread 源码中和线程优先级相关的属性有 3 个:

// 线程可以拥有的最小优先级

public final static int MIN_PRIORITY = 1;

   

// 线程默认优先级

public final static int NORM_PRIORITY = 5;

   

// 线程可以拥有的最大优先级

public final static int MAX_PRIORITY = 10

   

线程的优先级可以理解为线程抢占 CPU 时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行。

   

在程序中我们可以通过 Thread.setPriority() 来设置优先级,setPriority() 源码如下:

public final void setPriority(int newPriority) {

    ThreadGroup g;

    checkAccess();

    // 先验证优先级的合理性

    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {

        throw new IllegalArgumentException();

    }

    if((g = getThreadGroup()) != null) {

        // 优先级如果超过线程组的最高优先级,则把优先级设置为线程组的最高优先级

        if (newPriority > g.getMaxPriority()) {

            newPriority = g.getMaxPriority();

        }

        setPriority0(priority = newPriority);

    }

}

   

【线程的常用方法】

线程的常用方法有以下几个。

   

join()

一个线程中调用 other.join() ,这时候当前线程会让出执行权给 other 线程,直到 other 线程执行完或者过了超时时间之后再继续执行当前线程,join() 源码如下:

public final synchronized void join(long millis)

throws InterruptedException {

    long base = System.currentTimeMillis();

    long now = 0;

    // 超时时间不能小于 0

    if (millis < 0) {

        throw new IllegalArgumentException("timeout value is negative");

    }

    // 等于 0 表示无限等待,直到线程执行完为之

    if (millis == 0) {

        // 判断子线程 (其他线程) 为活跃线程,则一直等待

        while (isAlive()) {

            wait(0);

        }

    } else {

        // 循环判断

        while (isAlive()) {

            long delay = millis - now;

            if (delay <= 0) {

                break;

            }

            wait(delay);

            now = System.currentTimeMillis() - base;

        }

    }

}

   

从源码中可以看出 join() 方法底层还是通过 wait() 方法来实现的。

   

例如,在未使用 join() 时,代码如下:

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            for (int i = 1; i < 6; i++) {

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("子线程睡眠:" + i + "秒。");

            }

        });

        thread.start(); // 开启线程

        // 主线程执行

        for (int i = 1; i < 4; i++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("主线程睡眠:" + i + "秒。");

        }

    }

}

程序执行结果为:

复制主线程睡眠:1秒。

子线程睡眠:1秒。

主线程睡眠:2秒。

子线程睡眠:2秒。

主线程睡眠:3秒。

子线程睡眠:3秒。

子线程睡眠:4秒。

子线程睡眠:5秒。

   

从结果可以看出,在未使用 join() 时主子线程会交替执行。

   

然后我们再把 join() 方法加入到代码中,代码如下:

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {

            for (int i = 1; i < 6; i++) {

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                System.out.println("子线程睡眠:" + i + "秒。");

            }

        });

        thread.start(); // 开启线程

        thread.join(2000); // 等待子线程先执行 2 秒钟

        // 主线程执行

        for (int i = 1; i < 4; i++) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            System.out.println("主线程睡眠:" + i + "秒。");

        }

    }

}

程序执行结果为:

   

复制子线程睡眠:1秒。

子线程睡眠:2秒。

主线程睡眠:1秒。 

// thread.join(2000); 等待 2 秒之后,主线程和子线程再交替执行

子线程睡眠:3秒。

主线程睡眠:2秒。

子线程睡眠:4秒。

子线程睡眠:5秒。

主线程睡眠:3秒。

从执行结果可以看出,添加 join() 方法之后,主线程会先等子线程执行 2 秒之后才继续执行。

   

yield()

Thread 的源码可以知道 yield() 为本地方法,也就是说 yield() 是由 C C++ 实现的,源码如下:

public static native void yield();

   

yield() 方法表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。

   

比如我们执行这段包含了 yield() 方法的代码,如下所示:

public static void main(String[] args) throws InterruptedException {

    Runnable runnable = new Runnable() {

        @Override

        public void run() {

            for (int i = 0; i < 10; i++) {

                System.out.println("线程:" +

                        Thread.currentThread().getName() + " I" + i);

                if (i == 5) {

                    Thread.yield();

                }

            }

        }

    };

    Thread t1 = new Thread(runnable, "T1");

    Thread t2 = new Thread(runnable, "T2");

    t1.start();

    t2.start();

}

   

当我们把这段代码执行多次之后会发现,每次执行的结果都不相同,这是因为 yield() 执行非常不稳定,线程调度器不一定会采纳 yield() 出让 CPU 使用权的建议,从而导致了这样的结果。

posted @ 2020-06-18 11:08  流年的夏天  阅读(413)  评论(0编辑  收藏  举报