上下文切换是指 CPU 从一个线程转到另一个线程时,需要保存当前线程的上下文状态,恢复另一个线程的上下文状态,以便于下一次恢复执行该线程时能够正确地运行。
在多线程编程中,上下文切换是一种常见的操作,上下文切换通常是指在一人 CPU 上,由于多个线程共享 CPU时间片,当一个线程的时间片用完后,需要切换到另一个线程运行。此时需要保存当前线程的状态信息,包括程序计数器、寄存器、栈指针等,以便下次继续执行该线程时能够恢复到正确的执行状态。同时,需要将切换到的线程的状态信息恢复,以便于该线程能够正确运行。
在多线程中,上下文切换的开销比单线程大,因为在多线程中,需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统的运行效率,因此需要尽可能减少上下文切换的次数。
演示一个上下文切换场景,Code.1:
/**
* 这个示例使用了synchronized关键字和wait、notify方法来控制线程之间的同步
*/
public class ComplexContextSwitchExample {
private int count = 0;
private final Object lock = new Object();
public static void main(String[] args) {
ComplexContextSwitchExample example = new ComplexContextSwitchExample();
Thread thread1 = new Thread(example::increment, "Thread 1");
Thread thread2 = new Thread(example::increment, "Thread 2");
thread1.start();
thread2.start();
}
public void increment() {
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
count++;
System.out.println(Thread.currentThread().getName() + " incremented count to " + count);
// 释放锁,以便其他线程可以获取锁并执行
lock.notify();
}
// 让出CPU执行权,以便其他线程有机会执行
Thread.yield();
}
}
}
在这个Demo中,有一个共享变量count和一个锁对象lock。两个线程(线程1和线程2)都试图同时访问和修改count变量。为了确保线程安全,我们使用synchronized关键字和lock对象来同步对count变量的访问。在每个线程的increment方法中,我们在增加count变量之后调用lock.notify()方法来唤醒等待在锁上的其他线程。然后,我们使用Thread.yield()方法让出CPU执行权,以便其他线程有机会执行。这样,我们就模拟了一个更复杂的上下文切换场景,其中涉及到了线程同步和等待/通知机制。
频繁的上下文切换会导致CPU时间的浪费,因此在多线程编程时需要尽可能地避免它。以下是一些避免频繁上下文切换的方法:
- 减少线程数:可以通过合理的线程池管理来减少线程的创建和销毁,线程数不是越多越好,合理的线程数可以避免线程过多导致上下文切换。
- 使用无锁并发编程:无锁并发编程可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换的发生。
- 使用CAS算法: CAS算法可以避免线程的阻塞和唤醒操作,从而减少上下文切换。
- 使用协程(JDK 19的虚拟线程): 协程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免上下文切换。 (避免的是操作系统级别的上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多)
- 合理地使用锁:在使用锁的过程中,需要避免过多地使用同步块或同步方法,尽量缩小同步块或同步方法的范围,从而减少线程的等待时间,避免上下文切换的发生。
先来看第一个减少上下文切换的Code.2:
/**
* 使用了线程池来管理线程,以便更有效地复用线程资源
*/
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ContextSwitchReductionExample {
public static void main(String[] args) {
int numThreads = 10;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
for (int i = 0; i < numThreads; i++) {
executor.execute(new Task());
}
executor.shutdown();
}
}
class Task implements Runnable {
@Override
public void run() {
// 执行任务的代码
System.out.println(Thread.currentThread().getName() + " is running.");
}
}
在这个Demo中,使用Java的ExecutorService来创建一个固定大小的线程池。我们将要执行的Task对象提交给线程池,线程池会为每个任务分配一个线程来执行。由于线程池的大小是固定的,因此线程可以在任务之间复用,从而减少了上下文切换的次数。与直接创建多个线程相比,使用线程池可以更有效地管理线程资源,并减少上下文切换的开销。
先来看第贰个减少上下文切换的Code.3:
import java.util.concurrent.*;
/**
* 使用了线程池和Future来异步执行任务,并使用ExecutorCompletionService来获取任务的结果。
*/
public class ComplexContextSwitchReductionExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(5);
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executor);
for (int i = 0; i < 10; i++) {
final int taskId = i;
completionService.submit(() -> {
// 执行任务的代码
return "Task " + taskId + " completed.";
});
}
for (int i = 0; i < 10; i++) {
Future<String> future = completionService.take(); // 获取下一个完成的任务结果
System.out.println(future.get()); // 获取任务返回值
}
executor.shutdown();
}
}
在这个Demo中,使用Java的ExecutorService来创建一个固定大小的线程池。然后,我们使用ExecutorCompletionService来异步提交任务并获取任务的结果。通过使用ExecutorCompletionService,我们可以按任务完成的顺序获取结果,而不是等待每个任务完成。这样可以减少线程之间的上下文切换,并提高程序的效率。同时,我们还使用了Future来获取任务的返回值,这使得我们可以轻松地处理任务的异步执行结果。