Java 高并发核心编程 ----- 初识多线程(下)

文章目录

    • [3. 线程的核心原理](#3. 线程的核心原理)
        • [3.1 线程的调度与时间片](#3.1 线程的调度与时间片)
        • [3.2 线程的优先级](#3.2 线程的优先级)
        • [3.3 线程的生命周期](#3.3 线程的生命周期)
        • [3.4 使用 Jstack 工具查看线程状态](#3.4 使用 Jstack 工具查看线程状态)
    • [4. 线程的基本操作](#4. 线程的基本操作)
        • [4.1 线程名称的设置和获取](#4.1 线程名称的设置和获取)
        • [4.2 线程的 sleep 操作](#4.2 线程的 sleep 操作)
        • [4.3 线程的 interrupt 操作](#4.3 线程的 interrupt 操作)
        • [4.4 线程的 join 操作](#4.4 线程的 join 操作)
        • [4.5 线程的 yield 操作](#4.5 线程的 yield 操作)
        • [4.6 线程的 daemon 操作](#4.6 线程的 daemon 操作)

特此注明 :
Designed By :长安城没有风
Version:1.0
Time:2026.1.24
Location:四川 · 成都

本文为读者阅读《Java 高并发核心编程 卷2》(作者:尼恩)后摘抄部分段落以及整合个人理解后重写书写,推荐感兴趣的朋友可以阅读一下原著,如果有侵权可以私信作者进行删除。

3. 线程的核心原理

现在操作系统(如 Windows,Linux,Solaris)提供了强大的线程管理能力,Java不需要在进行自己独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如 Solaris 操作系统上),JVM 甚至将每个 Java 线程一对一地对应到操作系统的本地线程,彻底将线程调度委托给操作系统。

3.1 线程的调度与时间片

由于 CPU 的计算频率非常高,每秒计算数十亿次,因此可以将 CPU 的时间从毫秒的维度分段,每一小段叫做一个 CPU 的时间片。对于不同的操作系统,不同的 CPU,线程的 CPU 时间片长度都不同。假定操作系统(比如 Windows XP)线程的时间片长度为 20 毫秒,在一个 2GHz 的 CPU 上,一个时间片可以进行计算的次数是 20亿/(1000/20)= 4000万次,也就是说,一个时间片内的计算量是非常巨大的。

目前操作系统中主流的线程调度方式是:基于 CPU 时间片的方式进行线程调度。线程只有得到 CPU 的时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间来回切换,因此表现出来的特征是很多个线程在 "同时执行" 或者 "并发执行"。

线程调度的模型目前主要有两种:分时调度模型和抢占式调度模型。

  1. 分时调度模型:系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片的调度分配上,所有线程 "人人平等"。
  1. 抢占式调度模型:系统按照线程优先级分配时间片。优先级高的线程,优先分配 CPU 时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取 CPU 时间片相对多一些。

由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java 的线程管理和调度是委托给操作系统完成的,与之相对应,Java 的线程调度也是使用抢占式调度模型,因此 Java 的线程都有优先级。

3.2 线程的优先级

在 Thread 类中有一个实例属性和两个实例方法,专门用于进行线程优先级相关的操作,与线程优先级相关的成员属性与实例方法为:

java 复制代码
    private int priority;

    public final int getPriority() {
        return priority;
    }
    
    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);
        }
    }

Threads 实例的 priority 属性默认是级别5,对应的常量是 NORM_PRIORITY 。优先级最大值为 10,最小值为 1,Thread 类中定义的三个优先级常量如下:

java 复制代码
    /**
     * The minimum priority that a thread can have.
     */
    public static final int MIN_PRIORITY = 1;

    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;

Java中使用抢占式调度模型进行线程调度。priority 实例属性的优先级越高,线程获得 CPU 时间片的机会越多,但也不是绝对的。举一个例子,代码如下:

java 复制代码
import java.util.concurrent.*;

public class ThreadDemo {

    static class MyThread extends Thread{
        int count = 0;

        @Override
        public void run() {
            while(true && count<Integer.MAX_VALUE){
                count++;
            }
        }
    }

    public static void main(String[] args) {
        MyThread[] myThreads = new MyThread[10];
        for(int i=0;i<10;i++){
            myThreads[i] = new MyThread();
            myThreads[i].setPriority(i+1);
        }
        for(int i=0;i<10;i++){
            myThreads[i].start();
        }
        try{
            Thread.sleep(100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        for(int i=0;i<10;i++){
            myThreads[i].interrupt();
        }
        for(int i=0;i<10;i++){
            System.out.println("线程"+i+"的count值为:"+myThreads[i].count+"   线程优先级为:"+myThreads[i].getPriority());
        }
    }
}

演示实例中的 10 个线程停下来之后,某个线程的实例属性 count 的值越大,就表明该线程获得的 CPU 时间片越多。分析案例的执行结果,可以看出以下结论:

  1. 整体而言,高优先级的线程获得的执行机会更多。在实例中可以明显看到:优先级在 6 级以上的线程和 4 级以下的线程执行机会明显偏多,整体对比非常明显。
  2. 执行机会的获取具有随机性,优先级高的不一定获得的机会多。
3.3 线程的生命周期

Java 中的线程的生命周期分为 6 种状态。Thread 类有一个实例属性和一个实例方法专门用来保存和获取线程的状态。其中,用于保存线程 Thread 实例状态的实例属性为:

java 复制代码
private volatile int threadStatus; //以整数的形式保存线程的状态

public State getState() {
	return jdk.internal.misc.VM.toThreadState(threadStatus); //返回当前线程的执行状态,一个枚举类型值
}

public enum State {
	NEW, //新建
	RUNNABLE, //可执行:包含操作系统的就绪,运行两种状态
	BLOCKED, //阻塞
	WAITING, //等待
	TIMED_WAITING, //计时等待
	TERMINATED; //终止
}
  1. NEW 状态

Java 源码对 NEW 状态的注释说明是:创建成功但没有调用 start() 方法启动的 Thread 线程实例都处于 NEW 状态。当然,并不是 Thread 线程实例的 start() 方法一经调用,其状态就从 NEW 状态到 RUNNABLE 状态,此时并不意味着线程立即获取 CPU 时间片且立即执行,中间需要一系列的操作系统内部操作。

  1. RUNNABLE 状态

前面讲到,当调用了 Thread 实例的 start() 方法后,下一步如果线程获取到 CPU 时间片开始执行,JVM 将异步调用线程的 run() 方法执行其业务代码,那么在 run() 方法被异步调用之前,JVM做了哪些事情呢?

JVM 的幕后工作和操作系统的线程调度有关。Java 中的线程管理是通过 JNI 本地调用的方式,委托操作系统的线程管理 API 完成的。当 Java 线程的 Thread 实例的 start() 方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而 Java 线程就没有这个状态。操作系统中的就绪状态是什么状态呢?

一个操作系统线程如果处于就绪状态,表示 "万事俱备,只欠东风",即该线程已经满足了执行条件,但是还不能执行。处于就绪状态的线程需要等待系统的调度,一旦就绪状态被系统选中,获得 CPU 时间片,线程就开始占用 CPU,开始执行线程的代码,这时线程的操作系统状态发生了改变,进入了运行状态。

在操作系统中,处于运行状态的线程在 CPU 时间片用完之后,又回到就绪状态,等待 CPU 的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间被操作系统反复的调度,这种情况会一直持续,知道线程的代码逻辑执行完成或者异常终止。这时线程的操作系统状态又发生了改变,就如到了线程的最后状态 ----- TERMINATED 状态。

就绪状态和运行状态都是操作系统中的状态。在 Java 语言中,并没有细分这两种状态,而是将这两种状态合并为同一种状态 ----- RUNNABLE 状态。因此,在 Thread.State 枚举类中,没有定义线程的就绪状态和运行状态,只是定义了 RUNNABLE 状态。这就是 Java 线程状态和操作系统中的线程状态有所不同的地方。

总之,NEW 状态的 Thread 实例调用了 start() 方法后,线程的状态将变成 RUNNABLE 状态。尽管如此,线程的 run() 方法不一定会马上被并发执行,需要在线程获取了 CPU 时间片之后,才会真正启动并发执行。

  1. TERMINATED 状态

处于 RUNNABLE 状态的线程在 run() 方法执行完成之后就变成终止状态 TERMINATED 了。当然,如果在 run() 方法执行过程中发生了异常而没有被捕获,run() 方法将被异常终止,线程也会变成 TERMINATED 状态。

  1. TIMED_WATING 限时等待状态

线程处于一种特殊的等待状态,准确的说,线程处于限时等待状态。能让线程处于限时等待状态的操作大概有以下几种:

java 复制代码
Thread.sleep(int n) // 使得当前线程进入限时等待状态,等待时间为n毫秒
Object.wait() // 带时限的抢占对象的monitor锁
Thread.join() // 带时限的线程合并 
LockSupport.parkNanos() // 让线程等待,时间以纳秒为单位
LockSupport.parkUntil() // 让线程等待,时间可以灵活设置
3.4 使用 Jstack 工具查看线程状态

有时,服务器的 CPU 占用率会一直很高,甚至一直处于 100%。如果 CPU 使用率居高不下,自然是有某些线程一直占用着 CPU 资源,如何查看 CPU 占用率较高的线程呢?或者说,如何查看到线程的状态呢?一种比较快捷的办法是使用 Jstack 工具。

Jstack 工具是 Java 虚拟机自带的一种堆栈跟踪工具。Jstack 用于生成或导出(DUMP) JVM 虚拟机运行实例当前时刻的线程快照。线程快照是对当前 JVM 实例内每一个线程正在执行的方法的堆栈的集合,生成或导出线程快照的主要目的是用于定位线程出现长时间运行,停顿或阻塞的原因,如线程间死锁,死循环,请求外部资源导致的长时间等待等。线程出现停顿的时候通过 Jstack 来查看各个线程的调用堆栈,就可以知道没有相应的线程在后台做什么事情,或者等待什么资源。

bash 复制代码
Jstack 命令的语法格式:
jstack <pid>  // pid 表示 Java 进程id,可以使用 jps 命令查看

一般情况下,通过 Jstack 输出的线程信息主要包括:JVM 线程,用户线程等。其中 JVM 线程会在 JVM 启动时就存在,主要用于执行譬如垃圾回收,低内存的检测等后台任务,这些线程往往在 JVM 初始化的时候就存在。而用户线程则是在程序创建了新的线程时才会生成。这里要注意的是:

  1. 在实际运行时,往往一次 DUMP 的信息不足以确认问题。建议产生三次 DUMP 信息,如果每次 DUMP 都指向同一个问题,我们才能确定问题的典型性。
  2. 不同的 Java 虚拟机的线程导出来的 DUMP 信息格式是不一样的,并且同一 JVM 的不同版本 DUMP 信息也有差别。

Jstack 指令所输出的信息包含以下重要信息:

  1. tid:线程实例在 JVM 进程中的 id。
  2. nid:线程实例在操作系统中对应的线程底层的线程 id。
  3. prio:线程实例在 JVM 进程中的优先级。
  4. os_prio:线程实例在操作系统中对应的底层线程的优先级。
  5. 线程状态:如 RUNNABLE,BLOCKED 等。

用户线程往往是执行业务逻辑的线程,是大家所关注的重点,也是最容易产生死锁的地方,接下来的内容会用 Jstack 命令来分析用户线程的 WAITING,BLOCKED 两种状态。

4. 线程的基本操作

Java 线程的常用操作基本定义在 Thread 类中,包括一些重要的静态方法和线程实例方法。

4.1 线程名称的设置和获取

在 Thread 类中可以通过构造器 Thread(...) 初始化设置线程名称,也可以通过 setName(...) 实例方法去设置线程名称,取得线程名称可以通过 getName() 方法完成。

关于线程名称有以下几个要点:

  1. 线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。
  2. 允许两个 Thread 对象有相同的名称,但是应该避免。
  3. 如果程序没有为线程指定名称,系统会自动为线程设置名称。

说明:编程规范要求,创建线程或线程池时,需要指定有意义的线程名称,方便出错时回溯。

4.2 线程的 sleep 操作

sleep 的作用是让目前正在执行的线程休眠,让 CPU 去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。sleep() 方法定义在 Thread 类中,是一组静态方法,有两个重载版本:

java 复制代码
// 使目前正在执行的线程休眠 millis 毫秒
public static native void sleep(long millis) throws InterruptedException;

// 使目前正在执行的线程休眠 millis 毫秒,namos 纳秒
public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && millis < Long.MAX_VALUE) {
            millis++;
    }

    sleep(millis);
}

sleep() 方法会有 InterruptException 受查异常抛出,如果调用了 sleep() 方法,就必须进行异常审查,捕获 InterruptException 异常,或者再次通过方法声明存在 InterruptException 异常。

注意:当线程睡眠时间满后,线程不一定会立即得到执行,因为此时 CPU 可能正在执行其他的任务,线程首先进入的是就绪状态,等待分配 CPU 时间片以便有机会执行。

4.3 线程的 interrupt 操作

Java语言提供了 stop() 方法终止正在运行的线程,但是 Java 将 Thread 的 stop() 方法设置为过时,不建议大家使用。为什么呢?因为使用 stop() 方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机。在程序中,我们是不能随便中断一个线程的,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断线程可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断线程可能导致数据不一致的问题。正是由于使用 stop() 方法来终止线程可能会产生不可预料的结果,因此并不推荐调用 stop() 方法。

一个线程什么时候可以退出呢?当然只有线程自己知道。所以,这里介绍一下 Thread 的 interrupt() 方法,此方法本质不是用来中断一个线程,而是将线程设置为中断状态。

当我们调用线程的 interrupt() 方法时,它有两个作用:

  1. 如果此线程处于阻塞状态(如调用了 Object.wait() 方法),就会立马退出阻塞,并抛出 InterruptException 异常,线程就可以通过捕获 InterruptException 异常来做一定的处理,然后让线程退出。更确切的说,如果线程被 Object.wait(),Thread.sleep(),Thread.join() 三种方法之一阻塞,此时调用该线程的 interrupt() 方法,该线程将抛出一个 InterruptException 中断异常(该线程必须事先预备好处理此异常),从而过早终结被阻塞状态。

  2. 如果此线程正在运行中,线程就不会受任何影响,继续执行,仅仅是线程的中断标记被设置为 true。所以,程序可以在适当的位置通过调用 isInterruted() 方法来查看自己是否被中断,并执行退出操作。

说明:如果线程的 interrupt() 方法先被调用,然后线程开始调用阻塞方法进入阻塞状态,InterruptedException 异常依旧会抛出。如果线程捕获 InterruptException 异常后,继续调用阻塞方法,将不再触发 InterruptException 异常。

4.4 线程的 join 操作

线程的合并是一个比较难以说清楚的概念,什么是线程的合并呢?举一个例子,假设有两个线程 A 和 B,现在线程 A在执行过程中对另一个线程 B 的执行有依赖,具体的依赖为:线程 A 需要线程 B 的执行流程合并到自己的执行流程中,这就是线程合并,被动方线程 B 可以叫作被合并线程。这个例子中线程 A 合并线程 B 的伪代码大致为:

java 复制代码
public class ThreadA extends Thread{
	void run(){
		Thread threadB = new Thread();
		threadB.join();
	}
}

1. 线程的 join 操作的三个版本

join() 方法是 Thread 类的一个实例方法,有三个重载状态:

java 复制代码
	// 1. 此方法会把线程状态变成 WAITING_TIME 状态,直到被合并线程结束,或者等待被合并线程执行 mills 时间。
 	public final synchronized void join(final long millis)throws InterruptedException {
        if (millis > 0) {
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }

// 2. 此方法会把线程状态变成 WAITING_TIME 状态,直到被合并线程结束,或者等待被合并线程执行 mills+nanos 时间。
    public final synchronized void join(long millis, int nanos)throws InterruptedException {

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && millis < Long.MAX_VALUE) {
            millis++;
        }

        join(millis);
    }

   // 3. 此方法会把线程状态变成 WAITING 状态,直到被合并线程结束。
    public final void join() throws InterruptedException {
        join(0);
    }

为了方便表达,大家也可以将 join() 操作理解为等待操作,如果在线程 A 的代码中出现了 线程 B 的 join() ,就可以理解为线程 A 要等线程 B 执行完才可以继续执行,等待线程 B 执行的过程中 线程 A 的状态是 WAITING 或者 WATING_TIME,也可以参照下图进行理解:

2. 线程的 join 操作演示代码

java 复制代码
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() ->{
            for (int i = 0; i < 5; i++) {
                System.out.println("线程A正在运行"+i);
            }
        });

        Thread threadB = new Thread(() ->{
            for (int i = 0; i < 5; i++) {
                System.out.println("线程B正在运行"+i);
            }
        });
        threadB.start();
        threadA.start();
        threadB.join();
        System.out.println("主线程:threadB已执行完毕,开始收尾操作");
    }
}
4.5 线程的 yield 操作

线程的 yield (让步) 操作的作用是让目前正在执行的线程放弃当前的的执行,让出 CPU 的执行权限,使得 CPU 去执行其他的线程。处于让步状态的 JVM 层面的线程状态仍是 RUNNABLE 状态,但是该线程所对应的操作系统层面从状态上来说会从执行状态变成就绪状态。线程在 yield 时,线程放弃和重占 CPU 的时间是不确定的,可能是刚刚放弃 CPU,马上又获得 CPU 执行权限,重新开始执行。

Thread.yield() 方法有以下特点:

  1. yield 仅能使一个线程从运行状态转到就绪状态,而不是阻塞状态。
  2. 即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,也有可能选中刚刚切换状态的线程,其调度的过程受到其他因素(如优先级)的影响。
4.6 线程的 daemon 操作

Java 中的线程分为两类:守护线程与用户线程。守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个 JVM 进程,都会在后台运行着一系列的 GC(垃圾回收)线程,这些 GC 线程就是守护线程,提供幕后的垃圾回收服务。

举一个通俗易懂的例子,守护线程在 JVM 相当于保姆的角色:只要 JVM 实例中尚存在任何一个用户线程没有结束,守护线程就能执行自己的工作;只有当最后一个用户线程结束,守护线程随着 JVM 一同结束工作。

1. 守护线程的基本操作

在 Thread 类中有一个实例属性和两个实例方法,专门用于进行守护线程相关的操作:

java 复制代码
	// 保存一个 Thread 线程实例的守护状态,默认为 false,表示线程默认是用户线程
    private boolean daemon = false;

	// 此方法将线程设置为用户线程或者守护线程
    public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }

	// 获取线程的守护状态,用于判断该线程是不是守护线程
    public final boolean isDaemon() {
        return daemon;
    }

2.守护线程的要点

使用守护线程时,有以下几点需要特别注意:

  1. 守护线程必须在启动前将其守护状态设置为 true,启动之后不能再将用户线程设置为守护线程,否则 JVM 会抛出一个 InterruptException 异常。
  2. 守护线程存在被 JVM 强行终止的风险,所以在守护线程中尽量不去访问系统资源,如文件句柄,数据库连接等。守护线程被强行终止时,可能会引发系统资源操作不负责任的中断,从而导致资源不可逆的损坏。
  3. 在守护线程中创建的线程,新的线程都是守护线程。在创建之后,如果通过调用 setDaemon(false) 将新的线程显示地设置为用户线程,新的线程可以调整成用户线程。
相关推荐
余瑜鱼鱼鱼2 小时前
Thread类中run和start的区别
java·开发语言·前端
计算机程序设计小李同学2 小时前
基于位置服务的二手图书回收平台
java·前端·vue.js·spring boot·后端
青云交2 小时前
Java 大视界 -- 基于 Java+Flink 构建实时风控规则引擎:动态规则配置与热更新(446)
java·nacos·flink·规则引擎·aviator·实时风控·动态规则
想逃离铁厂的老铁2 小时前
Day51 >> 99、计数孤岛 + 100、最大岛屿面积
java·服务器
Java程序员威哥2 小时前
SpringBoot多环境配置实战:从基础用法到源码解析与生产避坑
java·开发语言·网络·spring boot·后端·python·spring
Thanwind2 小时前
系统可观测性解析与其常用套件
java
茶本无香2 小时前
设计模式之六—组合模式:构建树形结构的艺术
java·设计模式·组合模式
LJianK12 小时前
select .. group by
java·数据库·sql
猿小羽2 小时前
[TEST] Spring Boot 快速入门指南 - 1769246843980
java·spring boot·后端