线程调度与上下文切换
线程调度机制
什么是线程调度
对于单CPU计算机,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。宏观上看,各个线程轮流获得CPU使用权,分别执行各自的任务。
线程的运行状态包含两种子状态:就绪(READY)和运行中(RUNNING)。线程从就绪状态变为运行状态,需要系统调度分配CPU使用权。
线程调度是指按照特定机制为多个线程分配CPU使用权的过程。进程是资源分配的基本单元,线程是CPU调度的基本单元。
不同操作系统的线程调度
Linux线程调度
在Linux中,线程由轻量级进程(lightweight process)实现,线程调度采用进程调度方式。调度器根据线程的调度策略(scheduling policy)和静态调度优先级(static scheduling priority)决定运行哪个线程。
Linux主要有三种调度策略:
- SCHED_OTHER: 分时调度策略(默认)
- SCHED_FIFO: 实时调度策略,先到先服务
- SCHED_RR: 实时调度策略,时间片轮转
Windows线程调度
Windows采用基于优先级的抢占式调度算法。调度程序确保最高优先级的线程总是处于运行状态。
当低优先级线程运行时,如果更高优先级的线程变为就绪状态,低优先级线程会被抢占。这种抢占机制使得高优先级线程能优先获得CPU使用权。
调度程序选中的线程会持续运行,直到:
- 被更高优先级线程抢占
- 线程终止
- 时间片用完
- 调用阻塞系统调用(如I/O)
Java线程调度
Java程序运行在JVM上,JVM帮助屏蔽了操作系统差异,使Java成为跨平台语言。在操作系统中,一个Java程序就是一个进程,因此Java是单进程、多线程的。
Thread类的关键方法都声明为Native,需要根据不同操作系统有不同实现。JVM实现了线程调度器,定义了线程调度模型,规定了CPU运算的分配机制。
主要有两种调度模型:
协同式线程调度
协同式调度中,线程执行时间由线程自身控制。线程完成工作后,主动通知系统切换到其他线程。
优点:
- 实现简单
- 线程切换对线程自身可知,无线程同步问题
缺点:
- 线程执行时间不可控
- 一个线程可能导致整个进程阻塞
抢占式调度模型
抢占式调度中,每个线程的执行时间由系统分配,线程切换不由线程本身决定。系统让优先级高的线程占用CPU,优先级相同则随机选择。
优点:
- 线程执行时间可控
- 不会因一个线程导致整个进程阻塞
- 并发性更好
Java虚拟机采用抢占式调度模型。
线程优先级
虽然线程调度由系统自动完成,但可以通过设置线程优先级"建议"系统分配更多执行时间。Java设置了10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)。
public class PriorityExample {
public static void main(String[] args) {
Thread highPriority = new Thread(() -> {
System.out.println("高优先级任务执行");
});
Thread lowPriority = new Thread(() -> {
System.out.println("低优先级任务执行");
});
highPriority.setPriority(Thread.MAX_PRIORITY);
lowPriority.setPriority(Thread.MIN_PRIORITY);
highPriority.start();
lowPriority.start();
}
}
线程优先级并不完全可靠。Java线程通过映射到系统原生线程实现,线程调度最终取决于操作系统。虽然很多操作系统提供线程优先级概念,但不一定能与Java线程优先级一一对应。
上下文切换
什么是上下文切换
上下文切换是指CPU从一个线程转到另一个线程时,保存当前线程的上下文状态,恢复另一个线程的上下文状态的过程。
在多线程编程中,由于多个线程共享CPU时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括:
- 程序计数器
- 寄存器
- 栈指针
保存这些信息后,系统将另一个线程的状态信息恢复,使该线程能够正确运行。
上下文切换的开销
多线程环境下,上下文切换的开销比单线程大,因为需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统运行效率,导致CPU时间浪费。
减少上下文切换的方法
频繁的上下文切换会浪费CPU时间,在多线程编程时应尽可能避免。以下是减少上下文切换的方法:
减少线程数
通过合理的线程池管理减少线程的创建和销毁。线程数不是越多越好,合理的线程数可以避免因线程过多导致频繁上下文切换。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 根据CPU核心数设置合理的线程数
int coreNum = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(coreNum);
for (int i = 0; i < 100; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("执行任务 " + taskId);
});
}
executor.shutdown();
}
}
使用无锁并发编程
无锁并发编程可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换。
import java.util.concurrent.atomic.AtomicInteger;
public class LockFreeExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void increment() {
counter.incrementAndGet();
}
public static int getCount() {
return counter.get();
}
}
使用CAS算法
CAS(Compare And Swap)算法可以避免线程的阻塞和唤醒操作,减少上下文切换。
使用协程
协程是用户态线程,切换不需要操作系统参与,可以避免操作系统级别的上下文切换。
JDK 19引入的虚拟线程就是协程的一种实现。虚拟线程避免了操作系统级别的上下文切换,虽然仍需要在JVM层面保存和恢复线程状态,但成本低得多。
// JDK 19+ 虚拟线程示例
public class VirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread vThread = Thread.ofVirtual().start(() -> {
System.out.println("虚拟线程执行任务");
});
vThread.join();
}
}
合理使用锁
在使用锁时,需要:
- 避免过多使用同步块或同步方法
- 尽量缩小同步块或同步方法的范围
- 减少线程等待时间,避免上下文切换
public class LockOptimization {
private final Object lock = new Object();
private int sharedData = 0;
public void inefficientMethod() {
synchronized(lock) {
// 大量非共享数据操作
int localVar = 0;
for (int i = 0; i < 1000; i++) {
localVar += i;
}
// 共享数据操作
sharedData++;
}
}
public void efficientMethod() {
// 非共享数据操作放在锁外
int localVar = 0;
for (int i = 0; i < 1000; i++) {
localVar += i;
}
// 只对共享数据操作加锁
synchronized(lock) {
sharedData++;
}
}
}
线程存活状态判断
isAlive()方法
在Java中,可以通过Thread类的isAlive()方法判断线程是否存活:
public class AliveExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("工作线程开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("工作线程结束");
});
worker.start();
System.out.println("worker.isAlive() = " + worker.isAlive());
worker.join();
System.out.println("worker.isAlive() = " + worker.isAlive());
}
}
输出结果:
worker.isAlive() = true
工作线程开始
工作线程结束
worker.isAlive() = false
isAlive()方法的特殊情况
但isAlive()方法在某些情况下可能返回不准确的结果:
public class AliveSpecialCase {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
System.out.println("worker线程开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("worker线程结束");
});
Thread monitor = new Thread(() -> {
synchronized (worker) {
System.out.println("monitor线程开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("monitor线程结束");
System.out.println("worker.isAlive(): " + worker.isAlive());
}
});
worker.start();
monitor.start();
}
}
输出结果:
monitor线程开始
worker线程开始
worker线程结束
monitor线程结束
worker.isAlive(): true
可以看到worker线程已经结束,但isAlive()方法返回true。
原理分析
产生这个现象的原因是:isAlive()需要获取当前对象的锁。上面代码中monitor线程对worker对象进行了synchronized,即worker线程结束时需要修改自己的状态,而worker对象的锁被monitor持有,所以无法修改状态,导致isAlive()返回true。
查看isAlive()方法实现:
public final native boolean isAlive();
这是一个本地方法,对应JDK源码中的java_lang_Thread::is_alive方法。底层实现是获取当前线程对象的_eetop_offset值,不为空则返回true。
bool java_lang_Thread::is_alive(oop java_thread) {
JavaThread* thr = java_lang_Thread::thread(java_thread);
return (thr != NULL);
}
JavaThread* java_lang_Thread::thread(oop java_thread) {
return (JavaThread*)java_thread->address_field(_eetop_offset);
}
调用start()方法时,通过native_thread->prepare(jthread)的prepare方法设置_eetop_offset为当前线程对象:
void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) {
// 省略其他代码
java_lang_Thread::set_thread(thread_oop(), this);
// 省略其他代码
}
void java_lang_Thread::set_thread(oop java_thread, JavaThread* thread) {
java_thread->address_field_put(_eetop_offset, (address)thread);
}
Java线程结束时,JVM调用JavaThread::exit方法:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
// 省略其他代码
ensure_join(this); // 设置线程状态,包括设置_eetop_offset值为空
// 省略其他代码
}
static void ensure_join(JavaThread* thread) {
Handle threadObj(thread, thread->threadObj());
ObjectLocker lock(threadObj, thread); // 获取当前线程对象的锁
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL); // 设置_eetop_offset值为空
lock.notify_all(thread);
}
ObjectLocker就是synchronized的实现:
ObjectLocker::ObjectLocker(Handle obj, Thread* thread, bool doLock) {
_obj = obj; // obj为worker线程对象
if (_dolock) {
ObjectSynchronizer::fast_enter(_obj, &_lock, false, _thread);
// 由于worker的锁被monitor持有,worker无法设置_eetop_offset值为空
// 因此isAlive()返回true,线程状态也无法修改为TERMINATED
}
}
线程退出过程中需要获取当前对象的锁才能设置_eetop_offset。如果线程对象的锁被其他线程持有,线程无法设置_eetop_offset为空,此时isAlive()仍返回true,线程状态也无法修改为TERMINATED。