Java虚拟线程

这篇具有很好参考价值的文章主要介绍了Java虚拟线程。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录
  • Java虚拟线程
    • 简介
    • 线程(Thread)简介
    • 并行(parallel)与并发(concurrent)执行
    • 为什么要使用多线程?
      • 多线程如何影响系统的效率?
      • 对于唯一词计算效率又该如何呢?
      • 增加线程是否可以解决这个问题呢?
      • 线程内部是怎样工作的?
    • Java内部线程实现模式
      • 当前Java线程模型有什么问题吗?
      • 现实世界的例子
    • 虚拟线程(Virtual Thread)
    • 虚拟线程如何工作
      • Java虚拟线程实例
    • 绿色线程 VS 虚拟线程
    • 总结
    • 参考

Java虚拟线程

翻译自 screencapture-pradeesh-kumar-medium-an-era-of-virtual-threads-java

flowchart LR introduction-->a(why thread)-->b(parallel and concurrent execution)-->c(why threads?)-->d(how does thread work)-->e(java thread model)-->f(virtual thread)-->g(why virtual thread)-->h(comparasion) a-->c d-->f

简介

“虚拟线程”的概念越来越火,很多编程语言都尝试将其加入到线程库中,Java也不例外。JDK19中便加入了虚拟线程(预览版)JEP425。本文主要深入浅出介绍线程的前世今身,以及虚拟线程带来的全新体验和优势,最后会对几种不同的线程实现方式进行对比

线程(Thread)简介

一个电脑程序,本质上就是实现特定任务任务的一系列指令。当你加载一个程序时,操作系统加载程序文件,并将其放置在一个指定区域(地址空间),然后执行它所包含的指令。这时候,它就被作为了一个“进程”。换句话说,一个进程就是一个程序运行在电脑中的实例。

一个线程就是进程里的一系列可以独立运行的指令,通常线程在CPU的一个核上运行。一个进程可以拥有多个线程,允许多个线程同时执行即同时执行多个任务,这样可以更好的利用CPU资源,提高任务吞吐量。例如,当你加载谷歌浏览器,系统便创建了一个谷歌浏览器的进程。你可以同事做很多事情,比如说同时下载文件和浏览网页,因为这些功能运行在不同的线程之上。线程也可以叫做轻量级进程,因为线程之间共享了进程的地址空间。

flowchart LR disk--load-->memory--assign-->process process--include-->thread1 process--include-->thread2 process--include-->thread3 process--include-->thread4

并行(parallel)与并发(concurrent)执行

并行执行:同时执行多个任务。例如,一台四核的机器,可以每个核执行一个任务。所有的任务是同时执行的。

并发执行::电脑制造了一种同时执行任务数比CPU核数更多的错觉。例如,一个四核的电脑,可以执行8个不同的任务。因为你只有四核,所以必须做上下文切换来执行8个任务。在这里操作系统制造了一种并行执行8个任务的错觉。然而,实际上只有四个任务可以并行执行,因为只有四核。

并行执行图例:

flowchart LR core1-->thread1 core2-->thread2 core3-->thread3 core4-->thread4

并发执行图例:

flowchart LR core1-->a1(thread1)-->a2(thread4)-->a3(thread1)-->a4(thread8)-->a5(thread4) core2-->b1(thread2)-->b2(thread7)-->b3(thread2)-->b4(thread7)-->b5(thread2) core3-->c1(thread3)-->c2(thread5)-->c3(thread10)-->c4(thread3)-->c5(thread5) core4-->d1(thread5)-->d2(thread9)-->d3(thread5)-->d4(thread11)-->d5(thread1)

为什么要使用多线程?

首先,让我们研究一下线程是怎样提高我们的系统效率的。

假如你现在有一台四核的电脑,你要写一个两数之和的程序,同时要求必须执行在12个线程中。

代码如下:

public class SumOfNums {
    static void sum() {
        int a = 1;
        int b = 2;
        int sum = a + b;
        System.out.println(sum);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 12; i++) {
            Thread t = new Thread(SumOfNums::sum);
            t.start();
        }
    }
}

那么有多少线程可以并行执行呢?是否12个线程都可以并行执行?我们创建了12个线程,是否意味着12个线程是同时开始执行的?答案是否定的。我们的CPU只有四核,意味着我们并行执行的线程数上线就是四核。每个线程都必须分配到一个指定的核去执行,通过上下文切换来完成12个线程的并发执行。

那么为什么一个应用会有数百个线程?有什么用处?为什么我们不创建恰好等于CPU核数的线程?让我们更深入的研究一下。

任务通常有两种类型:CPU密集型以及IO密集型。

CPU密集型:任务执行高度依赖CPU,例如算术,逻辑,关联关系等,这些任务都是CPU密集型;

IO密集型:任务执行高度依赖输入/输出操作,例如网络交互,读取/存储文件,这些任务都是IO密集型;

那么我们上面提到的sum任务属于哪一种呢?我们创建并初始化了两个变量ab,将他们求和并输出到命令行。从始至终都没有任何的IO操作,数值计算是一个CPU操作,将结果写到两一个文件就是一个IO操作了。

通常来说,一个任务会混合CPU和IO操作。例如读取一个文本文件,并计算出其中所有的不同的单词,然后将结果写入到另一个文本文件。在这种情况下,读取和写入文件是一个IO操作,计算不同的单词是一个CPU操作。

多线程如何影响系统的效率?

再思考一下上面的CPU密集型求和代码。我们使用了12个线程并发执行这段代码。CPU内部切换前后台线程,这样就可以用4个核心完成12个线程的任务。如果还没有完成一个任务的时候,CPU会切换到另一个线程。

12个线程真的会提升效率吗?并非如此。当前的情况下,这里浪费了很多时间在切换上下文上。对于CPU密集型任务,最好使用和核心数统统的并发线程数,以达到最高的效率。

对于唯一词计算效率又该如何呢?

考虑这样一种情况,需要读取20个文本文件。CPU在读取文件时处于空闲状态,因为文件读取发生在硬盘驱动器上。所以只有当文件上下文都获取到了之后,CPU才会开始计算然后将结果写入到另一个文件。再写入过程中,CPU始终处于空闲状态,等待硬盘驱动器。

也就是说,我们执行的是如下图所示的单线程操作。

flowchart LR a(CPU)--wait-->b(__________IO__________)--hook-->c(CPU calculation)--wait-->d(____________IO__________)--hook-->e(CPU)

如上图所示,CPU在IO进行的时候都是空闲的。这导致了CPU的核心始终跑不满100%占用率。

现在,我们考虑一下当我们在4核心CPU上跑4个线程。如下图所示:

flowchart LR a1(CPU)--wait-->b1(__________IO__________)--hook-->c1(CPU calculation)--wait-->d1(____________IO__________)--hook-->e1(CPU) a2(CPU)--wait-->b2(__________IO__________)--hook-->c(CPU calculation)--wait-->d2(____________IO__________)--hook-->e2(CPU) a3(CPU)--wait-->b3(__________IO__________)--hook-->c3(CPU calculation)--wait-->d3(____________IO__________)--hook-->e3(CPU) a4(CPU)--wait-->b4(__________IO__________)--hook-->c4(CPU calculation)--wait-->d4(____________IO__________)--hook-->e4(CPU)

每一个线程被赋予了一个指定的核以并发执行。结果就导致了四核出现了与上面单核一样占用率不满的问题,IO期间核心依然是空闲的。

增加线程是否可以解决这个问题呢?

然我们来看一下如果我们使用八个线程会怎样:

flowchart LR core1-->a1(thread1)-->a2(thread2__)-->a3(_____thread1______)-->a4(__thread2__)-->a5(thread1) core2-->b1(thread3)-->b2(____thread4____)-->b3(thread3)-->b4(thread4)-->b5(thread2) core3-->c1(thread6)-->c2(thread5)-->c3(thread10)-->c4(thread3)-->c5(thread5) core4-->d1(thread5)-->d2(thread6)-->d3(thread5)-->d4(thread8)-->d5(thread1)

首先来看第一个核core1,当thread1进行IO操作时,core1此时是空闲的。然而,当存在多个未执行线程时,核心会切换到另一个线程开始执行,直到thread1完成了IO操作。这种方式可以最大化利用资源,提升多线程时的表现。

所以,在我们的第一个求和任务中,我们使用了比核心更多的线程数量没有获得效率提升,是因为浪费了过多的资源在上下文切换上。但是这里,8个线程提高了执行效率。我们从中可以学到,选择线程的数量,取决于IO操作频率和时间占用。IO操作占比越高,就需要越多的线程来提高执行效率。

线程内部是怎样工作的?

在我们继续研究虚拟线程前,必须要知道线程有哪些分类,以及他们怎么工作。

通常,现在操作系统中有两种不同的线程:核心线程以及用户线程

1. 核心线程

通常也叫做系统线程。核心线程通常由操作系统内核来安排管理。每个内核中的线程线程有一个TCB(Thread Control Block),其中包括了线程的优先级,状态,以及其它配置信息。核心线程是重量级的,需要系统调用来创建,调度,以及同步。

2. 用户线程

用户线程一般使用用户线程库进行管理和调度,不需要操作系统内核的干涉。内核不能意识到用户线程的变化。每个用户线程代表了一个应用中不同的数据结构,包括线程的状态信息和配置信息。用户线程是轻量级的,创建和销毁比系统线程更快。但是,依然会收到一些明确的限制,例如不能够享受到多处理器或者多核的优秀性能。

简单讲,当一个进程启动时,会启动一个默认线程,执行应用入口的main方法。随后,进程会创建自己需要的额外线程。用户线程是不能直接执行的,必须映射到一个制定的内核线程,然后通过内核线程执行执行。用户线程和内核线程的映射关系有以下三种:

  • M:1:所有的用户线程对应到一个内核线程上,通过库调度器进行调度;
  • 1:1:每个用户线程对应一个内核线程;
  • M:M:所有的用户线程映射到了一个内核线程池;

Java内部线程实现模式

绿色线程(Green Thread):远古时期,Java使用绿色线程模式。这个模式下,多线程的调度和管理有JVM完成。绿色线程模式才作用M:1线程映射模型。这里就有一个问题,Java不能够规模化管理这种线程,也就无法充分发挥硬件性能。同样的实现绿色线程也是一件非常有挑战性的事情,因为它需要非常底层的支持才能够良好运行。随后java移除了绿色线程,转而使用本地线程。这使得Java的线程执行比绿色线程更慢。

本地线程(Native Thread):从Java1.2开始从绿色线程切换到了本地线程模式。在操作系统的帮助下,JVM得以控制本地线程。本地线程的执行效率很高,但是开启和关闭他们的资源消耗较大。这就是为什么我们现在要使用线程池。这个模型遵循着1:1线程映射,即一个Java线程映射到一个内核线程。当一个java线程被创建时,相应的一个对应的核心线程也会被创建,用来执行线程代码。自此之后,本地线程模型的做法就延续到了今天。

当前Java线程模型有什么问题吗?

上面的章节中中,我们知道Java已经使用了本地线程模式。让哦我们看看这个模式有什么问题:

  • Java的线程库已经很老旧了;
  • 只是对于本地线程的一个简单包装;
  • 本地线程的创建和管理资源消耗较大;
  • 本地线程需要保存他们的调用栈在内存中,大概2MB~20MB的预留空间。如果你有4GB内存,如果每个线程占用20MB内存,那么你就只能创建大概200个线程;
  • 因为本地线程是一种系统资源,加载一个新的本地线程大概需要1毫秒;
  • 上下文切换代价昂贵,需要一个到内核的系统调用;
  • 上面这些强制性的限制会限制线程创建的数量,同时会导致性能下降和过度的内存消耗。因为我们不能创建更多的线程;
  • 我们不能通过增加更多的线程来增应用规模,因为上下文切换和内存占用的代价高昂;

现实世界的例子

考虑一台16GB内存的网络服务器。对于每个服务请求,都分配一个不同的线程。我们假设每个线程需要20MB内存空间,那么这台机器可以支持800个线程。当前,后端的API一般使用REST/SOAP调用方式,例如数据库操作和API信息转发这些IO密集型操作。由此可见,后端服务的主要是IO密集型而不是CPU密集型。

接着假设一下,一个IO操作需要100毫秒,请求执行(IO密集型)需要100毫秒,以及返回结果也需要100毫秒。同时,当每秒有800个请求时,线程数得到了最大容量。

让我们来计算一下单个请求的CPU占用时间

CPU时间 = 请求准备时间 + 返回结果准备时间
		= 0.1ms + 0.1ms
		= 0.2ms

对于800个请求呢?

800个线程的请求时间= 800 * 0.2ms
				= 160ms 

受限于我们的内存容量,我们只能创建800个请求,也就导致了我们CPU使用率并不高

PUC使用率=160ms / 1000ms
		= 16%

那么如何才能使CPU的利用率到达90%呢?

16% = 800个线程
90% = X个线程
X = 4500

但是我们当前因为内存的限制不能创建那么多的线程,除非我们能突破这个限制,拥有90G内存。

90G的内存是一个比较离谱的数字,所以说创建本地线程很明显不能充分利用硬件资源。

虚拟线程(Virtual Thread)

虚拟线程是一个Java线程的轻量级实现版本,最早于JDK19中出现,当前仍是预览状态,可以通过Jvm配置项开启。

虚拟线程是JVM项目loom的一部分

虚拟线程解决了传递和维护本地线程的瓶颈问题,同时可以用之编写高吞吐的并发应用,榨干硬件资源的潜力。

与本地线程不同,虚拟线程并不有操作系统控制,虚拟线程是一个有JVM管理的用户态线程。对比于本地线程的高资源占用,每个虚拟线程只需要几个字节的内存空间。这是的它更适合控制管理大量的用户访问,或者说处理IO密集型任务。

在创建虚拟线程的数量上几乎没有限制,甚至可以创建一百万个,因为虚拟线程并不需要来自内核的系统调用。

在虚拟线程如此轻量化的条件下,线程池不再成为必须品,只需要在需要的时候尽情创建虚拟线程就好。

虚拟线程和传统的本地线程操作完全兼容,例如本地线程变量,同步块,线程中断,等等。

虚拟线程如何工作

JVM管理着一个本地线程的线程池。一个虚拟线程想要进行CPU操作时,就把自己关联到一个池中本地线程的队列中。当虚拟线程中的CPU操作执行完毕后,JVM会自动解除关联并挂起该虚拟线程,同时切换到并执行另一个虚拟线程。这就是为什么我们可以创建很多的虚拟线程,并且他们是如此的轻量级。

JVM使用M:N来完成虚拟线程与本地线程的映射。

Java虚拟线程实例

  1. 在现存的线程使用新的ofVirtual()工厂方法:
for (int i = 0; i < 5; i++) {
    Thread vThread = Thread.ofVirtual().start(() -> System.out.println("Hellow World!!!"));
}
  1. 使用新的Executors工厂的newVirtualThreadExecutor()方法:
public static void main(String[] args) throws InterruptedException {
    var executor = Executors.newVirtualThreadPerTaskExecutor();
    for (int i = 0; i < 16; i++) {
        executor.submit(()-> System.out.println("Hello World!!!"));
    }
    executor.awaitTermination(1, TimeUnit.SECONDS);
    System.out.println("Finished");
}

绿色线程 VS 虚拟线程

线程类型 简介 映射
绿色线程 在一个系统线程上运行多个绿色线程 M:1
平台线程(Java当前使用) 系统线程的包装 1:1
虚拟线程 在多个系统线程中运行多个虚拟线程 M:N(M>N)

总结

总而言之,虚拟线程的新特性先对于传统多线程拥有很多优势。通过在用户空间提供的轻量化并发模型,虚拟线程使得编写并发程序更容易,使得大规模的线程并发成为可能。

参考

[1] screencapture-pradeesh-kumar-medium-an-era-of-virtual-threads-java文章来源地址https://www.toymoban.com/news/detail-471976.html

到了这里,关于Java虚拟线程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Java并发(二)----初次使用多线程并行提高效率

    并行代表充分利用多核 cpu 的优势,提高运行效率。 想象下面的场景,执行 3 个计算,最后将计算结果汇总。 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个

    2023年04月13日
    浏览(63)
  • Java多线程 -Thread类的常用API

    Thread常用API说明 : Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。 至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会在高级篇以及后续需要用到

    2024年02月21日
    浏览(41)
  • 【JavaEE】Java中的多线程 (Thread类)

    作者主页: paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文录入于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造,将基础知识一网打尽,希望可以帮到读者们哦。 其他专栏:《MySQL》《

    2024年02月05日
    浏览(48)
  • 【Java系列】详解多线程(二)——Thread类及常见方法(上篇)

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌 我们先来回顾一下线程与进程之间的联系。 我们知道多进程

    2024年02月04日
    浏览(41)
  • 【Java中的Thread线程的简单方法介绍和使用详细分析】

    提示:若对Thread没有基本的了解,可以先阅读以下文章,同时部分的方法已经在如下两篇文章中介绍过了,本文不再重复介绍!! 【Java中Tread和Runnable创建新的线程的使用方法】 【Java中的Thread线程的七种属性的使用和分析】 提示:以下是本篇文章正文内容,下面案例可供参

    2024年02月15日
    浏览(32)
  • 【Java系列】详解多线程(二)——Thread类及常见方法(下篇)

    个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【Java系列专栏】【JaveEE学习专栏】 本专栏旨在分享学习Java的一点学习心得,欢迎大家在评论区交流讨论💌 在操作系统中创建线程时,通常会同时创建相应的PCB并将其

    2024年02月04日
    浏览(32)
  • Java 高级应用-多线程-(一)实现 Runnable 接口与继承 Thread 类

    1.1 程序、进程与线程 • 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段 静态的代码,静态对象。 • 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行 中的 QQ,运行中的网易音乐播放器。 – 每个进程都有一

    2024年02月08日
    浏览(48)
  • 【Java练习题汇总】《第一行代码JAVA》多线程篇,汇总Java练习题——线程及多线程概念、Thread 类及 Runnable 接口、线程状态、synchronized同步操作...

    一、填空题 Java 多线程可以依靠________ 、________ 和________ 三种方式实现。 多个线程操作同一资源的时候需要注意________,依靠________ 实现,实现手段是:________ 和________,过多的使用,则会出现________ 问题。 Java 程序运行时,至少启动________ 个线程,分别是________ 和_

    2024年02月16日
    浏览(45)
  • JAVA深化篇_29—— 线程使用之线程联合以及Thread类中的其他常用方法【附有详细说明及代码案例】

    线程联合 当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。 join方法的使用 join()方法就是指调用该

    2024年02月05日
    浏览(36)
  • 【Java基础教程】(四十二)多线程篇 · 上:多进程与多线程、并发与并行的关系,多线程的实现方式、线程流转状态、常用操作方法解析~

    理解进程与线程的区别; 掌握Java 中多线程的两种实现方式及区别; 掌握线程的基本操作方法; 进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程 。多进程操作系统能同时运行多

    2024年02月16日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包