线程的状态与属性

一、线程的状态

线程的状态(生命周期)在 Thread 类中是一个枚举类,一共有六种状态,可以通过 Thread.getState() 方法获取当前线程的状态。每个线程同一时间只能处于一种状态,只可能有一次处于 NEW 状态和 TERMINATED 状态,此处的线程状态指的是在 JVM 中的线程状态。

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

public State getState() {
    return sun.misc.VM.toThreadState(threadStatus);
}

/src/share/classes/sun/misc/VM.java

private final static int JVMTI_THREAD_STATE_ALIVE = 0x0001; // 1
private final static int JVMTI_THREAD_STATE_TERMINATED = 0x0002; // 2
private final static int JVMTI_THREAD_STATE_RUNNABLE = 0x0004; // 4
private final static int JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER = 0x0400; // 1024
private final static int JVMTI_THREAD_STATE_WAITING_INDEFINITELY = 0x0010; // 16
private final static int JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT = 0x0020; //32

/**
 * Returns Thread.State for the given threadStatus
 */
public static Thread.State toThreadState(int threadStatus) {
    if ((threadStatus & JVMTI_THREAD_STATE_RUNNABLE) != 0) {
        return RUNNABLE;
    } else if ((threadStatus & JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER) != 0) {
        return BLOCKED;
    } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_INDEFINITELY) != 0) {
        return WAITING;
    } else if ((threadStatus & JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT) != 0) {
        return TIMED_WAITING;
    } else if ((threadStatus & JVMTI_THREAD_STATE_TERMINATED) != 0) {
        return TERMINATED;
    } else if ((threadStatus & JVMTI_THREAD_STATE_ALIVE) == 0) {
        return NEW;
    } else {
        return RUNNABLE;
    }
}

1.初始 NEW

初始状态,线程被创建,还没有调用 start() 方法,已经做了一些准备工作。由于一个线程实例只能被 start() 一次,所以一个线程只可能有一次处于该状态。

2.可运行 RUNNALBE

运行状态,一旦调用了 start() 方法,线程就会进入到可运行(RUNNABLE)状态, Java 线程将操作系统中的就绪(Ready)和运行(RUNNABLE)两种状态笼统地称作“可运行(RUNNALBE)”。也就是说 RUNNABLE 既可以是可运行的,也可以是实际运行的。有可能正在执行,有可能没有执行,没有执行的话,就是在等待 CPU 为它分配时间片。如果线程拿到 CPU 资源正在运行,此时 CPU 资源又被分配给别人,该线程此时还是处于可运行的状态。

  • READY:活跃线程,表示该状态的线程可以被线程调度器调度而使之处于 RNNING。
  • RUNNING:表示该状态的线程正在运行,即正在执行 run 方法。

3.阻塞 BLOCKED

阻塞状态,表示线程阻塞于锁。当一个线程进入到 synchronized 关键字修饰的方法或代码块时,而且该锁的 monitor 被其他线程给抢占了,此时该线程得不到该锁的 monitor,该线程就处于阻塞(BLOCK)状态。处于该状态的线程不会占用处理器资源,只有其他线程释放了该锁的 monitor,线程才能进入 RUNNALBE 状态。

阻塞状态是在进入 synchronized 关键字修饰的方法或代码块时的状态,其他锁造成的阻塞情况不属于 BLOCKED,如阻塞在 J.U.C 包中 LOCK 接口的线程状态却是等待状态,因为 J.U.C 包中的 Lock 接口对于阻塞的实现均使用了 LockSupport 类中的相关方法。

4.等待 WAITING

等待状态,表示线程进入等待状态,一个线程执行了某些特定方法后就会处于这种等待其他线程执行另外一些特定操作的状态,进入该状态表示当前线程需要等待其他线程做出一些特定工作(通知或中断),与阻塞不同的是,等待是主动的。如在某个对象上调用 Object.wait() 的线程正在等待另一个线程调用 Object.notify() 或 Object.notifyAll(),调用了 Thread.join() 的线程在等待指定线程的终止。

5.超时等待 TIMED_WAITING

超时等待状态,该状态不同于 WAITING,它是可以在指定的时间内自行返回的,不用等待其他线程的唤醒。

6.终止 TERMINATED

终止状态,已经执行结束的线程处于该状态,当线程执行正常结束或抛出异常时都会处于该状态。

7.线程的状态转换

NEW 只能到 RUNNABLE,只能从 RUNNABLE 到 TERMINATED。其他三种状态不能直接转换,只能通过 RUNNABLE 来进行转换。Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。通过两个例子分别来演示下线程的 6 种状态。

该段代码演示了 NEW、RUNNABLE 、TERMINATED 三种状态,输出结果分别为 NEW、RUNNALBE 和 TERMINATED。线程刚创建还未调用 start 方法时为 NEW 状态,调用了 start 方法后,状态为 RUNNABLE,等待 run 方法执行完毕后状态变为 TERMINATED。

public class NewRunnableTerminated implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new NewRunnableTerminated());
        // 打印出 NEW
        System.out.println(thread.getState());
        thread.start();
        // 打印出 RUNNABLE
        System.out.println(thread.getState());
        Thread.sleep(100);
        // 打印出 TERMINATED
        System.out.println(thread.getState());
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {}
    }
}

该段代码演示了 TIME_WAITING、WAITING、BLOCKED三种状态,输出结果分别为 TIMED_WAITING、BLOCKED、WAITING 和 WAITING 。线程1先启动,线程2后启动,线程1调用了 run 方法中的 sleep 函数,处于超时等待状态,此时线程1状态输出为 TIME_WAITING。然后线程2进入被 synchronized 修饰的代码块,但是线程1还没有释放锁的 monitor,所以线程2此时状态输出为 BLOCKED。主线程休眠6秒,使得线程1和线程2都休眠完毕并调用了 wait() 方法(线程 1 调用了 wait() 方法后会释放锁的 monitor),此时两个线程的状态都输出为 WAITING 。

public class BlockedWaitingTimedWaiting implements Runnable {

    private static BlockedWaitingTimedWaiting instance = new BlockedWaitingTimedWaiting();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(instance);
        thread1.start();
        Thread.sleep(500);
        Thread thread2 = new Thread(instance);
        thread2.start();
        Thread.sleep(500);
        
        // thread1 为 TIME_WAITING,因为正在执行 Thread.sleep(1000)
        System.out.println(thread1.getState());

        // thread2 为 BLOCKED,因为 thread2 想拿到 syn 方法的锁
        System.out.println(thread2.getState());

        Thread.sleep(6000);
        // 都打印出 WAITING
        System.out.println(thread1.getState());
        System.out.println(thread2.getState());
    }

    @Override
    public void run() {
        synchronized (instance) {
            try {
                Thread.sleep(3000);
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

二、线程的基本属性

线程的属性包括线程的编号(ID)、名称(Name)、线程类别(Daemon)和优先级(Priority)。

属性名称用途是否只读
编号(ID)用户标识不同的线程,不同的线程拥有不同的编号。
名称(Name)面向程序员的一个属性,用于区分不同的线程。
类别(daemon)true 表示为守护线程, false 表示用户线程。
优先级(Priority)该属性的本质是给线程调度器提示,用于表示应用程序希望哪个线程能够优先得以运行。

1.线程编号 ID

每一个线程都有自己的 ID,而且不能修改,如下源码所示,每次初始化线程都会生成一个线程编号,ID 初始值为 0,从 1 开始自增赋值给线程 ID。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    /* 省略 */
    // 设置线程 ID
    tid = nextThreadID();
}

/**
 * 线程 ID
 */
private long tid;


private static long threadSeqNumber;

/*
 * 用于自增线程ID,返回结果从 1 开始 
 */
private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

通过一个例子来演示线程 ID,该例子先输出主线程的 ID 然后再新建一个子线程输出子线程 ID。

public class ThreadId {

    public static void main(String[] args) {
        System.out.println("主线程的 ID:" + Thread.currentThread().getId());
        System.out.println("子线程的 ID:" + new Thread());
    }
}

输出结果如下。

主线程的 ID:1
子线程的 ID:11

发现主线程的 ID 为 1,而刚刚新建的子线程 ID 竟然是 11。因为一个 Java 程序的运行不仅仅是 main 方法的运行,而是 main 程序和多个其他线程同时运行。通过以下例子来测试下 main 函数启动的同时还有哪些线程。

public class MultiThreadId {

    public static void main(String[] args) {
        // 获取 Java 线程管理 ThreadMXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronized 信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程 ID 和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
        }
    }
}

输出结果如下。

[6]Monitor Ctrl-Break   // IDEA run 时开辟的线程,通过命令行的方式运行无该线程
[5]Attach Listener      // 接收外部命令,执行命令并返回结果
[4]Signal Dispatcher    // 把操作系统信号发送给 JVM 的线程
[3]Finalizer            // 负责执行对象的 finalize 相关的方法
[2]Reference Handler    // GC 引用相关的线程
[1]main                 // main 线程,用户程序入口

不同线程拥有的编号虽然不同,但是这种编号的唯一性只在 JVM 的一次运行中有效,也就是说重启 JVM 后,某些线程的编号可能与上次 JVM 运行的某个线程的编号一样,因此该属性的值不适合用作某种唯一标识。因为子线程的 ID 是从 11 开始,通过 jstack 查看 Java 线程,发现另外的 4 个线程如下。
在这里插入图片描述

2.线程名 Name

每个线程都有属于它默认的名字:“Thread-” + nextThreadNum(),从 Thread-0 开始,nextThreadNum() 方法的返回值从 0 开始,不同于 nextThreadID() 方法的返回值从 1 开始。通过带有线程名参数的构造器可以自定义线程名,Java 并不禁止我们将不同线程的名称设置为相同的值。尽管如此,设置明确的线程名称有助于代码调试和问题定位。

通过 getName 方法可以获取线程的名字,setName 方法修改线程的名字,不过从源码中可知,一旦线程启动了之后,threadStatus 不为 0,只能修改 JVM 层面线程的名字,无法修改操作系统层面的线程名。

/** 
 * 线程名 
 */
private volatile String name;

/**
 * 获取线程名
 */
public final String getName() {
	return name;
}

/**
 * 修改线程名
 */
public final synchronized void setName(String name) {
    checkAccess();
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
	// Java 内部线程的名字的话线程启动了也可以修改
    this.name = name;
    // 调用 native 在 c++ 层面去修改线程名字,一旦线程启动了,则不能修改了。
    if (threadStatus != 0) {
        setNativeName(name);
    }
}

private native void setNativeName(String name);

/**
 * 无参构造器,线程名默认
 */
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

/**
 * 带有线程名的构造器
 */
public Thread(String name) {
    init(null, null, name, 0);
}

/* 
 * 用于自动编号匿名线程
 */
private static int threadInitNumber;

/**
 * 用于线程名的编号,递增,返回值从 0 开始
 */
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

3.线程类别 Daemon

3.1 方法概览

按照线程是否会阻止 JVM 的正常停止,可以将 Java 中的线程分为守护线程(Daemon Thread)和用户线程(User Thread, 也叫非守护线程)。线程的 daemon 属性用于表示相应线程是否为守护线程。可以通过 isDaemon() 方法判断线程是否是守护线程。也可以通过 setDaemon(boolean on) 方法设置线程的类别,但是 daemon 属性需要在启动线程之前设置,因为在设置 daemon 属性时,会通过 isAlive() 方法检测线程是否活动,如果活动的话抛出 IllegalThreadStateException。

/**
 * true 为守护线程,false 为用户线程
 */
private boolean daemon = false;

/**
 * 判断是否是守护线程的方法
 */
public final boolean isDaemon() {
    return daemon;
}

/**
 * 判断线程是否是活动的
 */
public final native boolean isAlive();

/**
 * 设置守护线程
 */
public final void setDaemon(boolean on) {
    checkAccess();
    // 如果线程活动,则抛出异常
    if (isAlive()) {
        throw new IllegalThreadStateException();
    }
    daemon = on;
}

3.2 作用和特性

用户线程会阻止 JVM 的正常停止,即一个 JVM 只有在其所有用户线程都运行结束后才能正常停止。而守护线程则不会影响 JVM 的正常停止,应用程序中有守护线程在运行也不影响 JVM 的正常停止,即守护线程会和 JVM 一同退出。

JVM 的停止方式如下,如果 JVM 不是正常停止的,那么即使是用户线程也无法阻止 JVM 的停止。因此守护线程通常用于执行一些重要性不是很高的任务,例如用于监视其他线程的运行情况,垃圾处理器就是一个守护线程。

  • 正常停止:代码执行完毕。
  • 其他:通过调用 System.exit() 或者强制终止进程(如在 Linux 中使用 kill -9)。

接下来用一个例子来演示下守护线程的退出,定义了一个线程,该线程被设置为守护线程,run 方法中包含 try-catch-finally,按理说 finally 块一定会被执行,但是运行结果却没有任何输出。因为 main 线程在启动了线程 DaemonRunner 之后随着 main 方法的结束而执行完毕,此时 JVM 中已无用户线程,JVM 就退出了,所以守护线程 DaemonRunner 也就终止了,但是 finally 块中的代码并没有执行。因此在构建守护线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。而且基本上 JVM 自身提供的那些守护线程就够用了,一般不需要自行去建立守护线程。

public class Daemon {

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner());
        thread.setDaemon(true);
        thread.start();
    }

    private static class DaemonRunner implements Runnable {

        @Override
        public void run() {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("DaemonThread finally run.");
            }
        }
    }
}

在线程的创建中讲过子线程会继承父线程的 daemon、priority 等属性,所以用户线程创建出的线程默认就是用户线程,守护线程创建出的线程默认就是守护线程。

4.优先级 Priority

该属性本质上是给线程调度器提示,用于表示应用程序希望哪个线程能够优先得以运行。Java 的线程优先级包含了 10 个级别,最低优先级为 1,最高优先级为 10,默认优先级为 5。对于具体的一个线程而言,其默认的优先级与其父线程(创建该线程的线程)的优先级相等。可以通过 getPriority() 方法获取线程的优先级,也可以通过 setPriority(int) 方法修改线程的优先级。

/** 
 * 优先级
 */
private int priority;

/**
 * 最低优先级
 */
public final static int MIN_PRIORITY = 1;

/**
 * 默认优先级
 */
public final static int NORM_PRIORITY = 5;

/**
 * 最高优先级
 */
public final static int MAX_PRIORITY = 10;

/**
 * 获取优先级
 */
public final int getPriority() {
    return priority;
}

/**
 * 设置优先级
 */
public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    // 线程的优先级范围为 [1, 10]
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    // 判断所属线程组是否为空
    if((g = getThreadGroup()) != null) {
        // 新的优先级不能超过线程组的最大优先级
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        // 更新优先级
        setPriority0(priority = newPriority);
    }
}

private native void setPriority0(int newPriority);

现代操作基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片。线程会分配到若干个时间片,当线程的时间片用完了就会发生线程调度,并等待着下次的分配。线程分配到的时间片多少也就是决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多或者少分配一些处理器资源的线程属性。

设置线程优先级的时,针对频繁的阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被抢占。

不过程序的设计不应该依赖于优先级,在不同的 JVM 以及操作系统上,线程规划会存在差异,不同操作系统对优先级的理解不一样,有些操作系统甚至会忽略对线程优先级的设定。

通过一个例子来演示下线程优先级的作用,该段代码创建了 10 个线程,优先级分别从1-10,noStart 属性用于让 10 个线程都启动完毕才执行任务,当线程都启动完毕时,主线程休眠10秒,子线程通过调用 Thread.yield() 方法并不断自增 count,最后统计不同优先级线程执行的任务数量。

public class Priority {

    private static volatile boolean notStart = true;
    private static volatile boolean notEnd = true;

    public static void main(String[] args) throws InterruptedException {
        List<Job> jobs = new ArrayList<>();
        for (int priority = 1; priority <= 10; priority++) {
            Job job = new Job(priority);
            jobs.add(job);
            Thread thread = new Thread(job);
            thread.setPriority(priority);
            thread.start();
        }
        notStart = false;
        // main 线程休眠 10s
        TimeUnit.SECONDS.sleep(10);
        notEnd = false;
        jobs.forEach( job -> System.out.println("Job Priority : " + job.priority + ", Count : " + job.jobCount));
    }

    private static class Job implements Runnable {

        private int priority;
        private long jobCount;

        Job(int priority) {
            this.priority = priority;
        }

        @Override
        public void run() {
            // 通过 yield 等待 10 个线程都启动完毕
            while (notStart) {
                Thread.yield();
            }

            // 执行 10s,根据优先级来决定 jobCount 的大小
            while (notEnd) {
                Thread.yield();
                jobCount++;
            }
        }
    }
}

运行结果如下。

Job Priority : 1, Count : 2
Job Priority : 2, Count : 1
Job Priority : 3, Count : 2401
Job Priority : 4, Count : 2384
Job Priority : 5, Count : 307345
Job Priority : 6, Count : 307329
Job Priority : 7, Count : 1344579
Job Priority : 8, Count : 1343060
Job Priority : 9, Count : 2267088
Job Priority : 10, Count : 2263970

从输出中可以看出线程的执行任务数被划分为了 5 类,在不同操作系统或 JVM 版本上的输出结果可能不一样,笔者的环境为:Windows 10 1903,JDK 版本为 1.8.0_221-b11。通过 jstack 命令查看这 10 个线程的操作系统优先级如下,对于操作系统来说,这些优先级被分为了 5 类。

  • Thread-0,Thread-1,Java 线程优先级为 1,2,操作系统优先级为 -2
  • Thread-2,Thread-3,Java 线程优先级为 3,4,操作系统优先级为 -1
  • Thread-4,Thread-5,Java 线程优先级为 5,6,操作系统优先级为 -0
  • Thread-6,Thread-7,Java 线程优先级为 7,8,操作系统优先级为 1
  • Thread-8,Thread-9,Java 线程优先级为 9,10,操作系统优先级为 2

三、线程的层次关系

Java 平台中线程不是孤立的,线程 A 执行的代码创建了线程 B,那么,可以称线程 B 为线程 A 的子线程。子线程所执行的代码也可以创建其他子线程,父线程、子线程只是一个相对的称呼。这种父子关系称为线程的层次关系。

  • 默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。
  • 一个线程的优先级默认值为该线程的父线程的优先级。
  • 父线程和子线程的生命周期没有必然的联系。父线程运行结束后子线程可以继续运行。
  • 某些子线程也可以称为工作者线程(Worker Thread)或后台线程(Background Thread)。

《Java 并发编程的艺术》

《Java 多线程编程实战指南(核心篇)》

文章同步到公众号和Github,有问题的话可以联系作者。

灿烂一生
微信扫描二维码,关注我的公众号
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值