【Java并发篇】什么是多线程中的上下文切换?

在这里插入图片描述

多线程中的上下文切换

  • ✔️ 简述
  • ✔️拓展知识仓
    • ✔️减少上下文切换

✔️ 简述


上下文切换是指 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时间的浪费,因此在多线程编程时需要尽可能地避免它。以下是一些避免频繁上下文切换的方法:


  1. 减少线程数:可以通过合理的线程池管理来减少线程的创建和销毁,线程数不是越多越好,合理的线程数可以避免线程过多导致上下文切换。

  2. 使用无锁并发编程:无锁并发编程可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换的发生。

  3. 使用CAS算法: CAS算法可以避免线程的阻塞和唤醒操作,从而减少上下文切换。

  4. 使用协程(JDK 19的虚拟线程): 协程是一种用户态线程,其切换不需要操作系统的参与,因此可以避免上下文切换。 (避免的是操作系统级别的上下文切换,但是仍然需要在JVM层面做一些保存和恢复线程的状态,但是也成本低得多)

  5. 合理地使用锁:在使用锁的过程中,需要避免过多地使用同步块或同步方法,尽量缩小同步块或同步方法的范围,从而减少线程的等待时间,避免上下文切换的发生。

先来看第一个减少上下文切换的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来获取任务的返回值,这使得我们可以轻松地处理任务的异步执行结果。

你可能感兴趣的:(#,Java并发,java,开发语言)