- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
昨晚收到一个粉丝在私信的留言如下
build哥,今天参加了网易的提前批,可以说是一次惨痛的面试体验🤣,直接被虚拟线程问倒了,无论是在校学习的时候还是在公司实习的时候,都使用的是Java8更多,或者Java11,比较点子背的是面试我的这一个面试官,他们团队刚好在做Java21的切换,因此,虚拟线程似乎是一个逃脱不掉的重点拷问对象,虽然21出来的时候知道有虚拟线程这个事情,但从没有认真研究过,被问及时说不出个123来,当场憋得脸通红,真羞愧啊! 。
确实,我们现在在国内的大部分企业中使用的Java版本还是8居多,Java21是Oracle公司于2023年9月20号发布的版本,是一个最新且会被长期维护的稳定版本,很少有面试官会针对这部分更新内容着重拷问,但是!若你遇到了像这位粉丝一样,面试官的项目刚好在用Java21,那它的相关特性你就必须要知道了!而Java21带来的重磅内容就是虚拟线程。今天我们就抽个时间来聊一聊它.
虚拟线程 在Java19时被作为预览特性提出,经过了2个版本的迭代后,在Java21成功上位,是一个十分重要的新增特性,对于I/O密集型程序的性能带来了大幅度的提升! 。
随着企业应用的规模壮大,大量的网络请求或读写I/O场景越来越多,这种情况下,很多语言如Go、C#、Erlang、Lua等,都有“协程”来优化性能,曾经我们 Java 开发者面对这种平凡而又高级的技术只能干瞪眼,遇到I/O密集型程序,我们只能通过多线程来优化,实际上这种优化的效果有限,使用不当还会带来OOM问题,但在Java21推出虚拟线程后,一扫沉疴!虚拟线程的特性让它对高IO场景得心应手.
那么什么是虚拟线程呢?在搞清楚这个定义之前,我们先来了解一下普通线程,基于过往的学习积累,我们知道JVM 是一个多线程环境,它通过 java.lang.Thread 为我们提供了对 操作系统线程(OS线程) 的抽象,但是 Java 中的线程都只是对操作系统线程的一种简单封装,我们可以称之为 “平台线程(platform thread)” ,平台线程在底层 OS 线程上运行 Java 代码,并在代码的整个生命周期中占用该 OS 线程,因此平台线程的数量受限于 OS 线程的数量.
而虚拟线程是Thread的一个实例,虽然也在OS线程上运行Java代码,但它不会在整个生命周期内都占用该OS线程,换句话说,一个OS线程上支持多个虚拟线程的运行,因此,同样的操作系统配置下,可以创建更多的虚拟线程数量,执行阻塞任务的整体吞吐量也就大了很多.
【一句话总结虚拟线程定义】 。
虚拟线程: Java 中的一种轻量级线程,它旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,允许创建数千甚至数万个虚拟线程,而无需占用大量操作系统资源.
上面我们了解什么是虚拟线程后,我们紧接着来看一下它的原理,我们知道线程是需要被调度分配相应的CPU时间片的。对于由操作系统线程实现的平台线程,JDK 依赖于操作系统中的调度程序;而对于虚拟线程,JDK 先将虚拟线程分配给平台线程,然后平台线程按照通常的方式由操作系统进行调度.
JDK 的虚拟线程调度器是一个以 FIFO 模式运行的 ForkJoinPool,调度器的默认并行度是可用于调度虚拟线程的平台线程数量,并行度可以通过设置启动参数调整。调度器函数代码如下:
private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> {
PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
return AccessController.doPrivileged(pa);
};
PrivilegedAction<ForkJoinPool> pa = () -> {
int parallelism, maxPoolSize, minRunnable;
String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");
String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");
... //略过一些赋值操作
Thread.UncaughtExceptionHandler handler = (t, e) -> { };
boolean asyncMode = true; // FIFO
return new ForkJoinPool(parallelism, factory, handler, asyncMode,
0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);
};
return AccessController.doPrivileged(pa);
}
调度器分配给虚拟线程的平台线程称为虚拟线程的 载体线程(carrier),载体线程的信息对虚拟线程不可见,Thread.currentThread() 返回的值始终是虚拟线程本身,载体线程和虚拟线程的堆栈跟踪是分开的。在虚拟线程中抛出的异常将不包括载体线程的堆栈帧。线程dump不会在虚拟线程的堆栈中显示载体线程的堆栈帧,反之亦然。从 Java 代码的角度来看,开发者不能感知到虚拟线程和其载体线程临时共享了一个操作系统线程。但从本地代码(native code)的角度来看,虚拟线程和其载体在同一个本地线程上运行.
OS线程、载体线程、虚拟线程三者关系图 。
了解了虚拟线程之后,我们最重要的一环来了,如何使用虚拟线程!其实,Oracle官网在这一点上做的很人性化,为了让大家平滑的过渡到JDK21的使用上,虚拟线程的创建方式和之前的传统线程非常相似,几乎都是借助Thread来构建,大致分为如下4种方式.
方法1️⃣:使用 Thread.startVirtualThread() 创建 。
public void virtualThreadTest() {
Thread.startVirtualThread(() -> {
// 这里放置你的任务代码
System.out.println("Method ONE");
});
}
方法2️⃣:使用 Thread.ofVirtual()创建 。
public void virtualThreadTest() {
Thread.ofVirtual()
.name("virtualThreadTest")//为虚拟线程设置名称
.uncaughtExceptionHandler((t,e)-> System.out.println("线程[" + t.getName() + "发生了异常。message:" + e.getMessage()))//处理线程异常
.start(()->{
System.out.println("Method TWO");
});//创建时直接启动
}
Thread.Builder是一个流式API,用于构建和配置线程。它提供了设置线程属性(如名称、守护状态、优先级、未捕获异常处理器等)的方法。相比直接使用 Thread 来构建线程,Thread.Builder提供了更多的灵活性和控制力.
以上测试代码是创建时直接启动,也可以创建时不启动,通过手动调用 start() 来运行:
public void virtualThreadTest() {
var vt = Thread.ofVirtual()
.unstarted(()->{
System.out.println("Method TWO");
});
//创建时通过unstarted设置不启动,手动调用start启动
vt.start();
}
方法3️⃣:使用 ThreadFactory 创建 。
public void virtualThreadTest() {
ThreadFactory factory = Thread.ofVirtual().factory();
factory.newThread(() -> {
// 这里放置你的任务代码
System.out.println("Method THREE");
}).start();
}
方法4️⃣:使用 Executors.newVirtualThreadPerTaskExecutor()创建 。
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
System.out.println("Method FOUR");
});
or
Future<String> future = executor.submit(() -> {
return "Method FOUR";
});
这是通过虚拟线程池来构建虚拟线程; 注意:使用完线程池后,我们可以使用shutdown() 来关闭线程池,它会等待正在执行的任务完成,但不会接受新的任务。如果需要立即停止所有任务,可以使用 shutdownNow().
为了验证虚拟线程的优点,我们准备了一个小测试案例,向一个固定200个线程的线程池提交1000个sleep 1s的任务并遍历获取结果,用平台线程和虚拟线程分别实现,对比耗时.
public class TestPlatformThread {
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(0);
// 创建一个固定200个线程的线程池
try {
ExecutorService executor = Executors.newFixedThreadPool(200);
List<Future<Integer>> futures = new ArrayList<>();
long begin = System.currentTimeMillis();
// 向线程池提交1000个sleep 1s的任务
for (int i=0; i<1_000; i++) {
Future future = executor.submit(() -> {
Thread.sleep(1000);
return a.addAndGet(1);
});
futures.add(future);
}
// 获取这1000个任务的结果
for (Future<Integer> future : futures) {
Integer integer = future.get();
if(integer % 100 ==0){
System.out.println(integer + " ");
}
}
// 打印总耗时
System.out.println("Exec finish!!!");
System.out.printf("Exec time: %dms.%n", System.currentTimeMillis() - begin);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
输出:
100
200
300
400
500
600
700
800
900
1000
Exec finish!!!
Exec time: 5120ms.
这里为什么不采用Executors.newCachedThreadPool()而是采用固定线程数量的线程池呢? 因为当我们的并发任务数不是1000,而是1万,甚至于10万的时候,newCachedThreadPool会创建相应的线程数,而Java的平台线程于操作系统线程又是一一对应的,不可能提供那么多可用线程,会导致程序OOM.
我们将上述代码做一个简单的修改,采用Executors.newVirtualThreadPerTaskExecutor();创建一个虚拟线程池,通过虚拟线程进行同样的任务处理! 。
// ExecutorService executor = Executors.newFixedThreadPool(200);
//通过虚拟线程实现
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
输出:
100
200
300
400
500
600
700
800
900
1000
Exec finis!!!
Exec time: 1025ms.
一个是5秒多,一个是1秒多,当并发任务数来到100万的时候,虚拟线程耗时在27秒左右,而传统的平台线程,直接卡死,最终抛出OOM.
100000
200000
300000
400000
500000
600000
700000
800000
900000
1000000
Exec finis!!!
Exec time: 27125ms.
但是,我们在测试程序中是以sleep(1s)来模拟IO处理的场景,虚拟线程对性能的提升十分显著,若将程序中的sleep()换为如下代码:
long t0 = System .currentTimeMillis();
do {
int x=1;
x++;
} while (t0+1000 > System .currentTimeMillis());
以及模拟处理计算的场景,这时候耗时会反过来,下图为本地测试结果对比 。
最后,我们对虚拟线程的学习做一个言简意赅的总结:
优点:
缺点 。
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥! 。
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入! 。
最后此篇关于一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”的文章就讲到这里了,如果你想了解更多关于一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我将 Bootstrap 与 css 和 java 脚本结合使用。在不影响前端代码的情况下,我真的很难在css中绘制这个背景。在许多问题中,人们将宽度和高度设置为 0%。但是由于我的导航栏,我不能使用
我正在用 c 编写一个程序来读取文件的内容。代码如下: #include void main() { char line[90]; while(scanf("%79[^\
我想使用 javascript 获取矩阵数组的所有对 Angular 线。假设输入输出如下: input = [ [1,2,3], [4,5,6], [7,8,9], ] output =
可以用pdfmake绘制lines,circles和other shapes吗?如果是,是否有documentation或样本?我想用jsPDF替换pdfmake。 最佳答案 是的,有可能。 pdfm
我有一个小svg小部件,其目的是显示角度列表(参见图片)。 现在,角度是线元素,仅具有笔触,没有填充。但是现在我想使用一种“内部填充”颜色和一种“笔触/边框”颜色。我猜想line元素不能解决这个问题,
我正在为带有三角对象的 3D 场景编写一个非常基本的光线转换器,一切都工作正常,直到我决定尝试从场景原点 (0/0/0) 以外的点转换光线。 但是,当我将光线原点更改为 (0/1/0) 时,相交测试突
这个问题已经有答案了: Why do people write "#!/usr/bin/env python" on the first line of a Python script? (22 个回
如何使用大约 50 个星号 * 并使用 for 循环绘制一条水平线?当我尝试这样做时,结果是垂直(而不是水平)列出 50 个星号。 public void drawAstline() { f
这是一个让球以对角线方式下降的 UI,但球保持静止;线程似乎无法正常工作。你能告诉我如何让球移动吗? 请下载一个球并更改目录,以便程序可以找到您的球的分配位置。没有必要下载足球场,但如果您愿意,也可以
我在我的一个项目中使用 Jmeter 和 Ant,当我们生成报告时,它会在报告中显示 URL、#Samples、失败、成功率、平均时间、最短时间、最长时间。 我也想在报告中包含 90% 的时间线。 现
我有一个不寻常的问题,希望有人能帮助我。我想用 Canvas (android) 画一条 Swing 或波浪线,但我不知道该怎么做。它将成为蝌蚪的尾部,所以理想情况下我希望它的形状更像三角形,一端更大
这个问题已经有答案了: Checking Collision of Shapes with JavaFX (1 个回答) 已关闭 8 年前。 我正在使用 JavaFx 8 库。 我的任务很简单:我想检
如何按编号的百分比拆分文件。行数? 假设我想将我的文件分成 3 个部分(60%/20%/20% 部分),我可以手动执行此操作,-_-: $ wc -l brown.txt 57339 brown.tx
我正在努力实现这样的目标: 但这就是我设法做到的。 你能帮我实现预期的结果吗? 更新: 如果我删除 bootstrap.css 依赖项,问题就会消失。我怎样才能让它与 Bootstrap 一起工作?
我目前正在构建一个网站,但遇到了 transform: scale 的问题。我有一个按钮,当用户将鼠标悬停在它上面时,会发生两件事: 背景以对 Angular 线“扫过” 按钮标签颜色改变 按钮稍微变
我需要使用直线和仿射变换绘制大量数据点的图形(缩放图形以适合 View )。 目前,我正在使用 NSBezierPath,但我认为它效率很低(因为点在绘制之前被复制到贝塞尔路径)。通过将我的数据切割成
我正在使用基于 SVM 分类的 HOG 特征检测器。我可以成功提取车牌,但提取的车牌除了车牌号外还有一些不必要的像素/线。我的图像处理流程如下: 在灰度图像上应用 HOG 检测器 裁剪检测到的区域 调
我有以下图片: 我想填充它的轮廓(即我想在这张图片中填充线条)。 我尝试了形态学闭合,但使用大小为 3x3 的矩形内核和 10 迭代并没有填满整个边界。我还尝试了一个 21x21 内核和 1 迭代,但
我必须找到一种算法,可以找到两组数组之间的交集总数,而其中一个数组已排序。 举个例子,我们有这两个数组,我们向相应的数字画直线。 这两个数组为我们提供了总共 7 个交集。 有什么样的算法可以帮助我解决
简单地说 - 我想使用透视投影从近裁剪平面绘制一条射线/线到远裁剪平面。我有我认为是使用各种 OpenGL/图形编程指南中描述的方法通过单击鼠标生成的正确标准化的世界坐标。 我遇到的问题是我的光线似乎
我是一名优秀的程序员,十分优秀!