找回密码
 立即注册
首页 业界区 业界 技术面:Java并发(上下文切换、线程安全、并发与并行、 ...

技术面:Java并发(上下文切换、线程安全、并发与并行、守护线程、虚拟线程)

接快背 昨天 10:24
多线程中的上下文切换是什么?

上下文切换

是指CPU从一个线程切换到另一个线程时,需要保存当前线程的上下文状态,然后恢复另一个线程的上下文状态,这样下次恢复执行该线程时也能够正确的执行。
多线程中的上下文切换

在多线程的情况下,线程上下文的切换是一种常见的操作,通常是在一个CPU上,由于多个线程共享CPU的时间片,一个线程的时间片用完后,切换到另一个线程运行时,要保存当前线程的状态信息,包括程序计数器,寄存器,栈指针等。以便下次执行该线程时,能恢复到正确的状态
由于是在多线程的情况下,所以上下文切换的开销比单线程的开销大,因为多线程下需要保存和恢复更多的上下文信息。过多的上下文切换会降低系统的运行效率,因此需尽可能少的避免线程上下文切换次数。
避免频繁切换上下文的方法:


  • 降低线程数,通过合理的线程池来管理线程,减少线程的创建和销毁,并不是线程数越多越好,合理的线程数可以避免线程过多的上下文切换。
  • 采用无锁并发编程,可以避免线程因等待锁而进入阻塞状态,从而减少上下文切换的发生。
  • 用CAS算法,CAS这种乐观锁的算法,可以避免线程的阻塞和唤醒操作,从而减少上下文切换。
  • 合理是使用锁,在使用锁的过程中,避免过多地使用同步块或同步方法,一定要用的化,要尽量缩小同步块儿和同步方法的范围,从而减少线程的阻塞时间,减少上下文的切换。
  • 使用协程(JDK19的虚拟线程),这是一种用户态的线程,其切换不需要操作系统的参与,因此可以避免上下文的切换(JVM层面还是会有一些保存和恢复线程的状态)。
你觉得什么是线程安全?

线程安全是指在多线程并发的情况下,能够正确的处理多线程之间的共享变量,使程序能够正确的执行。
这里所说的程序能够正确执行,主要是满足所谓的原子性、有序性和可见性。
共享变量

共享变量,即所有线程都可以操作的变量。
在操作系统中,进程是分配资源的基本单位,线程是执行的基本单位,因此多个线程是可以共享进程中的数据。在JVM中,方法区的区域是多个线程共享的数据区域。
那么哪些变量是保存在堆和方法区中的呢,哪些变量又是保存在栈中的呢?
堆方法区(元空间)栈类变量实例变量局部变量
  1. public class VarTest {
  2.     /**
  3.      * 类变量
  4.      */
  5.     public static String ClassVar = "ClassVar";
  6.     /**
  7.      * 实例变量
  8.      */
  9.     public String entityVar = "entityVar";
  10.     /**
  11.      * 局部变量
  12.      */
  13.     public void logMethodVar(){
  14.         // 局部变量
  15.         int methodIntVar = 1;
  16.         System.out.println(methodIntVar);
  17.     }
  18. }
复制代码
并行和并发有什么区别

并发(concurrency),在操作系统中,同一时间有多个程序处于运行中,且这几个程序是在同一个CPU中执行。
对于单个CPU来说,同一时间只能干一件事情,为了看起来像是同事干多件事情,操作系统把CPU的时间划分成长短基本相同的时间区间,也就是“时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户使用。
并行(parallel),当操作系统有一个以上的CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互相不抢占CPU资源,可以同时进行。
1.png

2.png

两者的关键性区别


  • 并行是物理上的同时执行,而并发是逻辑上的同时执行。
  • 并行通常需要更多硬件资源(如多核CPU),并发则更注重任务调度的效率。
  • 并行的目标是加速单个任务(通过拆分任务并行处理),而并发的目标是处理更多任务(通过快速切换)。
守护线程与普通线程有什么区别

守护线程(Daemon Thread)和普通用户线程(User Thread),是两种不同类型的线程,两者都是可以通过Thread类或Runnable接口创建的。
两者最大的区别在于:JVM会等待所有普通用户线程执行完毕后才退出JVM不会等待守护线程完成,当所有普通线程结束时,JVM会强制终止所有守护线程。
守护线程一般被用来执行后台任务,最典型的场景就是JVM的GC(垃圾回收器)。
还有一些场景例如:

  • 日志记录(异步写入日志)
  • 定时任务监控(如心跳检测)
  • 资源清理(缓存清理)
  • JDK19出现的虚拟线程,也是守护线程。
普通线程
  1. public static void userThreadRun(){
  2.     Thread userThread = new Thread(() -> {
  3.         for (int i = 0; i < 3; i++) {
  4.             System.out.println("用户线程执行: " + i);
  5.             try {
  6.                 Thread.sleep(500);
  7.             } catch (InterruptedException e) {
  8.                 e.printStackTrace();
  9.             }
  10.         }
  11.     });
  12.     userThread.start();
  13. }
复制代码
运行结果
  1. 用户线程执行: 0
  2. 用户线程执行: 1
  3. 用户线程执行: 2
复制代码
守护线程执行
  1. public static void daemonThreadRun() throws InterruptedException {
  2.     Thread daemonThread = new Thread(() -> {
  3.         while (true) {
  4.             System.out.println("守护线程执行");
  5.             try {
  6.                 Thread.sleep(500);
  7.             } catch (InterruptedException e) {
  8.                 e.printStackTrace();
  9.             }
  10.         }
  11.     });
  12.     daemonThread.setDaemon(true);
  13.     daemonThread.start();
  14.     // 主线程(用户线程)执行完后,JVM退出,守护线程被强制终止
  15.     Thread.sleep(3000);
  16.     System.out.println("主线程结束");
  17. }
复制代码
运行结果
  1. 守护线程执行
  2. 守护线程执行
  3. 守护线程执行
  4. 守护线程执行
  5. 守护线程执行
  6. 守护线程执行
  7. 主线程结束
复制代码
守护线程daemonThread会无限循环打印,但当主线程结束时,守护线程会被JVM强制终止。
JDK21中的虚拟线程是什么?

虚拟线程?协程?

虚拟线程可能比较陌生,但是如果说叫协程是不是听着就比较熟悉了,如果有了解Go、Ruby、Python语言的,对协程肯定不陌生了。
Java中在JDK21中,正式将协程以虚拟线程的形式发布出来了。在之前的JDK版本中Java的线程模型比较简单,每一个Java线程对应一个操作系统中的轻量级进程,这种线程模型中的线程创建、析构及同步等动作,都需要进行系统调用。而系统调用则需要在用户态(User Mode)和内核态(KerneMode)中来回切换,所以性能开销还是很大的。
虚拟线程,是JDK 实现的轻量级线程,可以避免上下文切换带来的的额外耗费。实现原理其实是JDK不再是每一个线程都一对一的对应一个操作系统的线程了,而是会将多个虚拟线程映射到少量操作系统线程中,通过有效的调度来避免那些上下文切换。
3.png

虚拟线程和普通线程的区别


  • 虚拟线程总是守护线程。setDaemon(false)方法不能将虚拟线程更改为非守护线程。所以,需要注意的是,当所有启动的非守护线程都终止时,JVM将终止。这意味着JVM不会等待虚拟线程完成后才退出。
  • 即使使用setPriority()方法,虚拟线程始终具有normal的优先级,且不能更改优先级。在虚拟线程上调用此方法没有效果。
  • 虚拟线程是不支持stop()、suspend()或resume()等方法。这些方法在虚拟线程上调用时会抛出UnsupportedOperationException异常。
虚拟线程的使用

在JDK21中创建虚拟线程的方式有以下几种

  • 通过Thread.startVirtualThread方式
  1. Thread.startVirtualThread(() -> {
  2.     System.out.println("hello world I am a VirtualThread");
  3. });
复制代码

  • 通过Thread.Builder.OfVirtual方式
  1. Thread.Builder.OfVirtual myVirtualThread = Thread.ofVirtual().name("my-virtual-thread");
  2. myVirtualThread.start(() -> {
  3.     System.out.println("hello world I am a VirtualThread from Thread.Builder.OfVirtual");
  4. });
复制代码

  • 线程池也支持虚拟线程了,也可以通过Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程
  1. try(var executors = Executors.newVirtualThreadPerTaskExecutor()){
  2.     IntStream.range(0,100).forEach(i -> executors.execute(() -> {
  3.         System.out.println("hello world I am a VirtualThread from Executors.newVirtualThreadPerTaskExecutor(),"+i);
  4.         try {
  5.             Thread.sleep(1000);
  6.         } catch (InterruptedException e) {
  7.             throw new RuntimeException(e);
  8.         }
  9.     }));
  10. }
复制代码
但是,官方并不建议虚拟线程和线程池一起用,主要就是不想让虚拟线程进行池化,因为像所有资源池一样、线程池旨在共享昂贵的资源,但虚拟线程并不昂贵,因此永远不需要将它们池化。
实际来对比一下性能

先创建一个简单的任务
  1. public class TestTask implements Runnable{
  2.     public void baseTask(){
  3.         IntStream.range(0,100).forEach(i -> {
  4.             double a = Math.pow(i,2);
  5.         });
  6.         try {
  7.             Thread.sleep(10);
  8.         } catch (InterruptedException e) {
  9.             Thread.currentThread().interrupt();
  10.         }
  11.     }
  12.     @Override
  13.     public void run() {
  14.         baseTask();
  15.     }
  16. }
复制代码
先用普通线程执行一遍任务,统计耗时
  1. public void testUserTask(){
  2.     // 100个线程,100个任务,普通线程来执行
  3.     ExecutorService executorService = Executors.newFixedThreadPool(100);
  4.     long start = System.currentTimeMillis();
  5.     IntStream.range(0,100).forEach(i -> {
  6.         executorService.submit(new TestTask());
  7.     });
  8.     executorService.shutdown();
  9.     try {
  10.         executorService.awaitTermination(Long.MAX_VALUE,java.util.concurrent.TimeUnit.MILLISECONDS);
  11.     } catch (InterruptedException e) {
  12.         throw new RuntimeException(e);
  13.     }
  14.     System.out.println("user thread cost:"+(System.currentTimeMillis()-start));
  15. }
复制代码
运行结果
  1. user thread cost:177ms
复制代码
用虚拟线程再执行一遍
  1. public void tesVirtualTask(){
  2.     long start = System.currentTimeMillis();
  3.     IntStream.range(0,1000).forEach(i -> {
  4.         Thread.startVirtualThread(() -> {
  5.             new TestTask().run();
  6.         });
  7.     });
  8.     System.out.println("virtual thread cost:"+(System.currentTimeMillis()-start)+"ms");
  9. }
复制代码
运行结果
  1. virtual thread cost:53ms
复制代码
177ms缩减至53ms,效果非常显著!
4.png

来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除

相关推荐

您需要登录后才可以回帖 登录 | 立即注册