深入理解Java线程执行原理

深入理解Java线程执行原理

1.线程调度与执行

在学习线程的核心原理之前,我们先来了解一下 线程的调度 和执行

  • 线程调度器
  • 线程优先级与调度策略
  • Java虚拟如何调度线程执行

在Java中,线程调度与执行是指Java虚拟机(JVM)根据一定的调度算法和优先级来决定哪个线程在特定时刻执行。线程调度是多线程并发执行的关键部分,它决定了线程在CPU上执行的顺序和时间片分配。

1. 线程调度器

Java线程调度器负责协调和安排多个线程的执行顺序。其主要任务包括:

  • 决定哪个线程能够执行:线程调度器根据线程的优先级和调度算法来确定下一个执行的线程。
  • 切换线程的上下文:当一个线程被暂停执行并且另一个线程开始执行时,线程调度器负责上下文切换。
2. 线程优先级与调度策略

Java中的线程调度器根据线程的优先级决定哪个线程将会获得CPU的时间片。线程优先级是一个整数值,在Thread类中用常量表示,范围从1(最低优先级)到10(最高优先级)。

  • 调度策略:Java线程调度器遵循不同的调度策略,例如抢占式调度和时间片轮转调度等。抢占式调度指的是优先级高的线程会抢占CPU资源,而时间片轮转调度是将CPU时间分成若干个时间片,轮流分配给不同的线程。
  • 优先级的影响:虽然线程优先级可以影响线程调度的行为,但它并不是强制性的规定。具有更高优先级的线程更有可能获得CPU时间片,但并不意味着低优先级的线程永远无法执行。
3. Java虚拟机如何调度线程执行

Java虚拟机通过内置的线程调度器来管理和调度线程。线程调度器会根据线程的状态、优先级以及调度策略来确定哪个线程应该被执行。JVM会周期性地检查各个线程的状态并根据需要进行线程的调度。

  • 调度器的作用:调度器负责选择合适的线程执行,并根据需要进行上下文切换。
  • 线程状态的影响:线程的状态(就绪、运行、阻塞等)会影响调度器的决策,例如处于就绪状态的线程更容易被调度执行。
  • 实现细节:具体的线程调度实现细节取决于Java虚拟机的实现,不同的JVM可能采用不同的调度算法和优化策略。

Java线程调度与执行是Java并发编程中的重要组成部分,理解其原理可以帮助开发者编写高效、可靠的多线程程序。

2.时间片

目前操作系统的主流线程的调度方式是,基于CPU时间片方式进行线程调度,就是当只有线程得到CPU的时间片才能执行指令,当线程处于执行的状态,但是确没有分配到时间片,那么就会处于就绪状态,等待系统分配下一个时间片

但是呢,由于时间片的时间是非常短暂的,在各个线程之间快速的切换,会给我们一种错觉,“多个线程在同时执行或者并发执行”

线程的调度模型目前主要分为两种

  1. 分时调度模型
  2. 抢占式调度模型

下面我们就来了解一下,这两种调度模型,首先登场的是分时调度模型

2.1.分时调度模型

分时调度模型:系统平均分配CPU的时间片,所有线程轮流等待CPU分配时间片,每个线程分配到的时间片都是均等的。

分时调度模型是操作系统中一种常见的调度策略,它主要用于多任务环境下的 CPU 资源分配。其核心思想是将 CPU 时间分成若干个时间片,然后依次分配给不同的线程或进程使用,以实现多任务间的轮流执行,从而给用户提供了同时运行多个程序的错觉。

下面是分时调度模型的一些更为详细的介绍

  1. 时间片轮转:系统将 CPU 的运行时间划分为固定长度的时间片,每个时间片结束时,当前运行的线程会被暂停,然后调度器会选择下一个就绪的线程继续执行。这种方式保证了每个线程都能够在一定时间内获得 CPU 的使用权,而且每个线程获得的时间片是相等的。
  2. 公平性:分时调度模型追求公平性,即每个线程都有相同的机会获得 CPU 时间。由于每个线程都有固定长度的时间片,因此不会出现某个线程长时间占用 CPU 的情况,从而避免了资源独占和饥饿的问题。
  3. 实时性要求低:分时调度模型适用于对实时性要求不高的系统环境,因为在该模型下,没有对线程的优先级进行显式的调度,所有线程都以相同的优先级进行调度。这意味着无法保证对实时任务的及时响应。
  4. 简单高效:分时调度模型实现简单且高效,适用于一般性的多任务系统。它不需要复杂的调度算法,只需维护一个就绪队列,按照先来先服务的原则进行调度即可。
  5. 上下文切换开销:由于每个时间片结束时都需要进行上下文切换,因此分时调度模型可能会引入一定的开销。上下文切换是指保存当前执行线程的状态,并加载下一个线程的状态到 CPU 中,这个过程会消耗一定的系统资源。

image-20240309090107438

2.2.抢占式调度模型

系统会按照线程的优先级来分配CPU的时间片,优先级高的线程,那么会优先分配CPU时间片,如果所有线程的优先级一样的话,那么会随机选择一个,优先级高的线程获取CPU时间片就相对来说多一些

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

下面是抢占式调度模型的一些详细介绍:

  1. 基于优先级的调度:在抢占式调度模型中,系统会根据线程的优先级来分配 CPU 时间片。优先级高的线程会被优先执行,并且会获取更多的 CPU 时间片。这种方式可以确保系统能够优先处理具有更高重要性或紧急性的任务。
  2. 动态调度:抢占式调度模型允许操作系统在任何时刻中断当前正在执行的线程,并将 CPU 时间片分配给具有更高优先级的线程。这种动态调度机制可以及时响应系统中出现的紧急情况或重要任务,提高系统的响应速度和处理效率。
  3. Java线程调度:在 Java 编程语言中,线程调度和管理是由操作系统完成的,Java 线程调度器会委托给底层操作系统来完成。因此,Java 中的线程调度也是基于抢占式调度模型的,线程具有优先级,优先级高的线程会优先执行。
  4. 上下文切换开销:与分时调度模型类似,抢占式调度模型也会引入上下文切换的开销。由于系统需要在不同线程之间频繁切换 CPU 时间片,因此会消耗一定的系统资源。尽管抢占式调度模型能够提高系统的灵活性和响应能力,但过多的上下文切换可能会影响系统的性能。
  5. 竞争与饥饿问题:由于抢占式调度模型会优先执行优先级高的线程,低优先级的线程可能会面临饥饿的问题,即长期无法获得 CPU 时间片。为了避免饥饿现象的发生,系统需要采取一些策略,如定期提升低优先级线程的优先级,以确保所有线程都能够得到执行的机会

image-20240309091120159

image-20240309091030701

image-20240309091041943

3.线程优先级

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

// Thrad提供的默认优先级数值是 5 ,同时也为我们提供了 最大 最小优先级的常量
/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;

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

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

// 该属性保存一个Thread实例的优先级 1-10之间的值  数值越大 优先级越高
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);
    }
}
private native void setPriority0(int newPriority);


虽然Java中使用的是抢占式调度模型,priority实例属性的优先级越高,线程获取CPU时间片的机会越多,但是也是不对绝对的,因为线程获取CPU时间片具有随机性,获取CPU时间片还可能会受到以下因素影响。

  1. 操作系统调度策略影响Java 线程是由操作系统调度的,操作系统可能会考虑到其他因素,如线程的等待时间、线程的历史执行情况等因素来决定线程的调度顺序。
  2. Java 线程优先级的限制:Java 中的线程优先级是由操作系统支持的,不同的操作系统可能对线程优先级的支持程度有所不同。某些操作系统可能会限制线程优先级的最大值或最小值,从而影响到线程的实际执行顺序。
  3. 线程优先级不应滥用:虽然可以通过设置线程优先级来影响线程的调度顺序,但是过度依赖线程优先级可能会导致程序的不稳定性和可移植性问题。应该谨慎地使用线程优先级,并且尽量避免在程序中依赖线程优先级来控制线程的执行顺序。

下面我通过一个例子来演示一下

	@Test
	public void test1(){
		Thread t1 = new Thread(() -> {
			logger.error("当前线程名:{},开始执行任务!", Thread.currentThread().getName());
		}, "my-thread-1-线程");
		Thread t2 = new Thread(() -> {
			logger.error("当前线程名:{},开始执行任务!", Thread.currentThread().getName());
		}, "my-thread-2-线程");
		Thread t3 = new Thread(() -> {
			logger.error("当前线程名:{},开始执行任务!", Thread.currentThread().getName());
		}, "my-thread-3-线程");


		// 为 t1 t2 t3 设置优先级
		// t1 优先级最高
		t1.setPriority(Thread.MAX_PRIORITY);
		// t2 优先级普通
		t2.setPriority(Thread.NORM_PRIORITY);
		// t3 优先级最低
		t3.setPriority(Thread.MIN_PRIORITY);

		// 启动线程 我们来观察一下
		t1.start();
		t2.start();
		t3.start();
	}

image-20240309092734134

4.使用Jstack工具查看线程的状态

有的时候,我们的服务器的cpu的占用率一直会很高,甚至一直处于100%的状态,如果CPU的使用率一直居高不下,自然是某些线程一直占用CPU的资源,如何查看CPU占用率较高的线程呢?

一种比较快速便捷的方法就是使用Jstack

jstack 是 Java Development Kit (JDK) 中的一个命令行工具,用于生成 Java 进程的线程快照。通过 jstack,您可以获取 Java 应用程序运行时的线程堆栈信息,包括每个线程的调用栈信息、线程状态以及锁信息等。

下面是Jstack常见的用法

  1. 获取 Java 进程的进程 ID(PID):使用操作系统提供的任务管理器、jps 命令等工具,获取您要分析的 Java 进程的进程 ID(PID)。例如,在命令行中执行 jps 命令可以列出当前系统中所有 Java 进程及其 PID。

    jps
    
  2. 执行 jstack 命令:在命令行中执行以下命令,将 <pid> 替换为您要分析的 Java 进程的 PID。

    jstack <pid>
    

    执行该命令后,jstack 会生成当前 Java 进程的线程堆栈信息,并将其输出到标准输出。

  3. 分析线程堆栈信息jstack 输出的信息包括每个线程的 ID、状态、调用栈信息等。您可以通过分析每个线程的调用栈信息来了解线程当前正在执行的代码路径。调用栈信息显示了方法调用的顺序,从而可以追踪到导致问题的代码路径。

  4. 识别线程状态:根据 jstack 输出的信息,您可以识别每个线程的状态。常见的线程状态包括:

    • RUNNABLE:线程正在运行或者正在等待 CPU 时间片。
    • BLOCKED:线程被阻塞,等待获取对象的锁。
    • WAITING:线程正在无限期等待另一个线程执行特定操作。
    • TIMED_WAITING:线程正在等待另一个线程执行特定操作,但是有超时限制。
    • TERMINATED:线程已经结束执行。

    通过识别线程状态,可以帮助您了解线程的当前执行情况和可能的阻塞情况。

  5. 检测死锁情况:通过 jstack 输出的线程堆栈信息,您可以检测 Java 应用程序中的死锁情况。死锁通常是由于多个线程相互等待对方持有的锁资源而导致的。通过分析线程的等待情况和锁信息,可以识别和解决死锁问题。

  6. 结合其他诊断工具jstack 是 Java 开发工具包(JDK)中的一个简单但功能强大的工具,但它通常与其他监控和诊断工具结合使用。比如,结合使用 VisualVM、JConsole 等工具可以更全面地监控和诊断 Java 应用程序的运行情况。

下面我们通过一个案例来使用一下jstack

编写java文件

  1 public class HelloWorld {
  2     public static void main(String[] args) {
  3         while(true){
  4            System.out.println("Hello,World!");
  5         }
  6     }
  7 }        

编译Java文件为clsss文件

[root@server-1 java_demo]# javac HelloWorld.java

image-20240309101424291

执行java程序

这种方式有一个弊端,会直接从控制台运行 Java 程序则会将程序的输出直接打印到当前的终端中,程序会随着终端的关闭而终止。

如果需要记录日志,需要手动重定向输出。这个时候我们可以使用 nohup

[root@server-1 java_demo]# java HelloWorld

使用 nohup 命令

nohup 是一个 Unix/Linux 操作系统下的命令,用于在后台运行命令,即使用户退出登录或关闭终端,命令仍然会继续运行。

nohup java HelloWorld > output.log 2>&1 &
  • nohup: 这个命令允许您在后台运行程序,即使当前会话关闭,程序也会继续运行。
  • java HelloWorld : 启动您的 Java 应用程序。确保HelloWorld已经经过javac编译
  • > output.log 2>&1: 将标准输出和标准错误重定向到文件 output.log 中。
    • 2>&1 是一个 shell 命令中的重定向操作符,用于将标准错误流(stderr,文件描述符 2)重定向到标准输出流(stdout,文件描述符 1)。
    • 2 表示标准错误流(stderr)。
    • > 是输出重定向操作符,用于重定向输出流。
    • &1 表示将标准错误流重定向到文件描述符 1,也就是标准输出流。
    • 简单点说2>&1 将标准错误流重定向到与标准输出流相同的地方,这意味着标准输出和标准错误都会输出到同一个地方。
  • &: 将命令放入后台执行。

启动完成后,会返回给我们一个进程的 pid

image-20240309095558367

此时我们先来观察一下CPU的使用率(此时,我的风扇已经开始狂转了)

image-20240309101027471

image-20240309095835936

再通过jps查看一下java相关进程

image-20240309100153434

通过jstack查询对应进程信息,下面这个是我的打印出相关信息

[root@server-1 java_demo]# jstack 6738
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.391-b13 mixed mode):

"Attach Listener" #9 daemon prio=9 os_prio=0 tid=0x00007fd0fc001000 nid=0x1aa2 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007fd13c0c5000 nid=0x1a5f runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007fd13c0c0000 nid=0x1a5e waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fd13c0be000 nid=0x1a5d waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fd13c0bb000 nid=0x1a5c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fd13c0b9800 nid=0x1a5b runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fd13c086800 nid=0x1a5a in Object.wait() [0x00007fd12c5f4000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e3805078> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:150)
        - locked <0x00000000e3805078> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:171)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:188)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fd13c082000 nid=0x1a59 in Object.wait() [0x00007fd12c6f5000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000e3805230> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000e3805230> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

       
// "main" 线程:可运行状态,正在执行某个方法,其中涉及文件输出操作。该线程被阻塞在输出操作上。
// 因为我们写了while(true)循环一直打印,然后启动时,设置了将打印信息输出到日志,所有这里main线程会一直在执行某个操作
"main" #1 prio=5 os_prio=0 tid=0x00007fd13c009800 nid=0x1a53 runnable [0x00007fd145408000]
   java.lang.Thread.State: RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x00000000e3818c10> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x00000000e38052b0> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x00000000e3805268> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.newLine(PrintStream.java:546)
        - eliminated <0x00000000e38052b0> (a java.io.PrintStream)
        at java.io.PrintStream.println(PrintStream.java:807)
        - locked <0x00000000e38052b0> (a java.io.PrintStream)
        at HelloWorld.main(HelloWorld.java:4)

"VM Thread" os_prio=0 tid=0x00007fd13c078000 nid=0x1a58 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007fd13c01e800 nid=0x1a54 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007fd13c020800 nid=0x1a55 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007fd13c022800 nid=0x1a56 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007fd13c024000 nid=0x1a57 runnable

"VM Periodic Task Thread" os_prio=0 tid=0x00007fd13c0c8000 nid=0x1a60 waiting on condition

JNI global references: 5
  1. “Attach Listener” 线程:等待条件下可运行,是用于处理 Java 进程的附加操作的守护线程。
  2. “Service Thread” 线程:可运行状态,是 JVM 内部的服务线程,执行低优先级的后台任务。
  3. “C1 CompilerThread2”、“C2 CompilerThread1”、“C2 CompilerThread0” 线程:这些是 JIT 编译器的线程,用于将 Java 字节码编译为本地代码。
  4. “Signal Dispatcher” 线程:可运行状态,用于处理 JVM 接收到的操作系统信号。
  5. “Finalizer” 线程:等待条件下可运行,负责执行对象的 finalize 方法。
  6. “Reference Handler” 线程:等待条件下可运行,用于处理引用对象的队列。
  7. “main” 线程:可运行状态,正在执行某个方法,其中涉及文件输出操作。该线程被阻塞在输出操作上。 因为我们写了while(true)循环一直打印,然后启动时,设置了将打印信息输出到日志,所有这里main线程会一直在执行某个操作
  8. “VM Thread”、“GC task thread”、“VM Periodic Task Thread” 线程:这些线程是 JVM 内部的垃圾回收线程和周期性任务线程,它们都处于可运行状态。

杀死进程

[root@server-1 java_demo]# kill -9 6738

image-20240309101228799

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值