Virtual Threads/Project Loom/Fiber

Virtual Threads即协程,将在Java 21 GA,针对这两年一直追的内容,做一些总结。 在Java中与Virtual Threads 相关的还有Structured Concurrency 和 Scoped Values,前者简化了对VT多线程的管理,而后者可以视为VT下的ThreadLocal。下面是摘录的各种文档。 为了管理多线程,可以使用Structured Concurrency 针对需要线程内共享的需求,可使用ScopedValues 在Spring Boot 3.2之前,可以这样配置 Web Servers & Task Execution使用VT 从Spring Boot 3.2起,只需要一条配置即可,会应用到Servlet Web Servers, Task Execution, Task Scheduling, Blocking Execution with Spring WebFlux, Http-NIO of Reactor, RabbitMQ/Kafka listener

Java反射 与 Lambda 函数映射

一、用「 Lambda 生成函数映射」代替「高频的反射操作」 我们来看一段最简单的反射执行代码: 上面的反射执行代码可以被改写成这样: fastjson2 中的具体实现的要复杂一点,但本质上跟上面一样,其本质也是生成了一个 function。 我们使用反射获取到的 Method 和 Lambda 函数分别执行 10000 次来看下处理速度差异: 处理速度相差居然达到 25 倍,使用 Java8 Lambda 为什么能提升这多呢? 答案就是:Lambda 利用 LambdaMetafactory 生成了函数映射代替反射。 下面我们详细分析下 Java反射 与 Lambda 函数映射 的底层区别。 1、反射执行的底层原理 注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节 如果要一句话解释的话,那就是通过元数据找到对应的方法,检查系统状态和反射参数没问题后就执行那个方法。 也就是说,正常执行一个方法的路径: java代码 => class字节码 => JVM解析字节码执行类加载过程 => 类加载获得元数据,JVM执行到对应的方法时根据元数据找到对应的方法开始执行 而反射执行的路径: java代码 => class字节码 => JVM解析字节码执行类加载过程 => JVM执行到反射部分的代码 => 通过反射系统拿到对应方法的元数据 => 检查系统状态和参数可以执行这个方法 => […]

详解Java lambda表达式

转自 Y组合子 函数式接口 Stream的实现原理 函数式编程(上) 函数式编程(下) Java 8引入了不少新特性,Function Interface, Stream, lambda,配合着范型,在看源码时会比较吃力,这篇文章前面大部分很基础,在上面的link中都有,后半部分结合源码和字节码稍有深度。比较推荐上面美团的函数式编程的讲解,虽然不太容易懂 本文的脉络 Lambda介绍 何为lambda 咱们首先来说说 Lambda 这个名字,Lambda 并不是一个什么的缩写,它是希腊第十一个字母 λ 的读音,同时它也是微积分函数中的一个概念,所表达的意思是一个函数入参和出参定义,在编程语言中其实是借用了数学中的 λ,并且多了一点含义,在编程语言中功能代表它具体功能的叫法是匿名函数(Anonymous Function),根据百科的解释: 匿名函数(英语:Anonymous Function)在计算机编程中是指一类无需定义标识符(函数名)的函数或子程序。 接着再来说说Lambda 的历史,虽然它在 JDK8 发布之后才正式出现,但是在编程语言界,它是一个具有悠久历史的东西,最早在 1958 年在Lisp 语言中首先采用,而且虽然Java脱胎于C++,但是C++在2011年已经发布了Lambda 了,但是 JDK8 的 LTS 在2014年才发布,所以 Java 被人叫做老土不是没有原因的,现代编程语言则是全部一出生就自带 Lambda 支持,所以Lambda 其实是越来越火的一个节奏~ Lambda 在编程语言中往往是一个匿名函数,也就是说Lambda 是一个抽象概念,而编程语言提供了配套支持,比如在 Java 中其实为Lambda 进行配套的就是函数式接口,通过函数式接口生成匿名类和方法进行Lambda 式的处理。 那么,既然是这一套规则我们明白了,那么Lambda 所提供的好处在Java中就是函数式接口所提供的能力了,函数式接口往往则是提供了一些通用能力,这些函数式接口在JDK中也有一套完整的实践,那就是 Stream。 不同语言中的Lambda Python 例子: C++ […]

ForkJoin简解

以下文章来源于冰河技术 转自 虽然关于ForkJoin的看了不少,这个还是不错的,比较简单的讲解 stream原理 写在前面 在JDK中,提供了这样一种功能:它能够将复杂的逻辑拆分成一个个简单的逻辑来并行执行,待每个并行执行的逻辑执行完成后,再将各个结果进行汇总,得出最终的结果数据。有点像Hadoop中的MapReduce。 ForkJoin是由JDK1.7之后提供的多线程并发处理框架。ForkJoin框架的基本思想是分而治之。什么是分而治之?分而治之就是将一个复杂的计算,按照设定的阈值分解成多个计算,然后将各个计算结果进行汇总。相应的,ForkJoin将复杂的计算当做一个任务,而分解的多个计算则是当做一个个子任务来并行执行。 Java并发编程的发展 对于Java语言来说,生来就支持多线程并发编程,在并发编程领域也是在不断发展的。Java在其发展过程中对并发编程的支持越来越完善也正好印证了这一点。 Java 1 支持thread,synchronized。 Java 5 引入了 thread pools, blocking queues, concurrent collections,locks, condition queues。 Java 7 加入了fork-join库。 Java 8 加入了 parallel streams。 Java 19 加入了 virtual threads 并发与并行 并发和并行在本质上还是有所区别的。 并发 并发指的是在同一时刻,只有一个线程能够获取到CPU执行任务,而多个线程被快速的轮换执行,这就使得在宏观上具有多个线程同时执行的效果,并发不是真正的同时执行,并发可以使用下图表示。 并行 并行指的是无论何时,多个线程都是在多个CPU核心上同时执行的,是真正的同时执行。 分治法 基本思想 把一个规模大的问题划分为规模较小的子问题,然后分而治之,最后合并子问题的解得到原问题的解。 步骤 ①分割原问题; ②求解子问题; ③合并子问题的解为原问题的解。 我们可以使用如下伪代码来表示这个步骤。 在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。 典型应用 二分搜索 大整数乘法 Strassen矩阵乘法 […]

Java语法糖 Syntactic Sugar

本文从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。 但只关注语法糖也不是好事,还有别的更重要的东西,就Java而言,虚拟机是其优势,但近期的版本调整的不太多了。另外就是诸如协程这种比较重要的特性。语言只是实现的工具,未来也许会接触更多的语言,有时候一些问题换个语言能有更 cool 的 scenario 解语法糖 前面提到过,语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。 说到编译,大家肯定都知道,Java语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。 如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。 Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。 糖块一、 switch 支持 String 与枚举 前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。 在开始coding之前先科普下,Java中的swith自身原本就支持基本类型。比如int、char等。 对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。 所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii码是整型)以及int。 那么接下来看下switch对String得支持,有以下代码: 反编译后内容如下: 看到这个代码,你知道原来字符串的switch是通过equals()和hashCode()方法来实现的。还好hashCode()方法返回的是int,而不是long。 仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。 糖块二、 泛型 我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的。 通常情况下,一个编译器处理泛型有两种方式:Code specialization和Code sharing。 C++和C#是使用Code specialization的处理机制,而Java使用的是Code sharing的机制。 Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。 也就是说,对于Java虚拟机来说,他根本不认识Map map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。 类型擦除的主要过程如下:(类型擦除是个大问题,有空还是看看Go之类的吧,相当长时间里,泛型都是Java的痛) […]

MethodHandles

Method Hanldes是在Java 7引入的概念。全限定名是java.lang.invoke.MethodHandles。 1.介绍 Method Handles的引入是为了与已经存在的java.lang.reflect API相配合。他们分别是为了解决不同的问题而出现的。从性能角度上说,MethodHandle api要比反射快很多因为访问检查在创建的时候就已经完成了,而不是像反射一样等到运行时候才检查。但同时,Method Handles比反射更难用,因为没有列举类中成员,获取属性访问标志之类的机制。另外,MethodHandles可以操作方法,更改方法参数的类型和他们的顺序。而反射则没有这些功能。从以上角度看,反射更通用,但是安全性更差,因为可以在不授权的情况下使用反射对象。而method Handles遵从了分享者的能力。所以method handle是一种更低级的发现,适配和调用方法的方式,唯一的优点就是更快。所以反射更适合主流Java开发者,而method handle更适用于对编译和运行性能有要求的人。 MethodHandle是可对直接执行的方法(或域、构造方法等)的类型的引用,或者说,它是一个有能力安全调用方法的对象。换个方法来说,通过句柄我们可以直接调用该句柄所引用的底层方法。作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步: (1)创建MethodType对象,指定方法的签名; (2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle; (3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。 MethodType:是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型。它的类型完全由参数类型和方法类型来确定,而与它所引用的底层的方法的名称和所在的类没有关系。可以通过MethodHandle类的type方法查看其类型,返回值是MethodType类的对象。也可以在得到MethodType对象之后,调用MethodHandle.asType(mt)方法适配得到MethodHandle对象。可以通过调用MethodType的静态方法创建MethodType实例,有三种创建方式: (1)methodType及其重载方法:需要指定返回值类型以及0到多个参数; (2)genericMethodType:需要指定参数的个数,类型都为Object; (3)fromMethodDescriptorString:通过方法描述来创建。 需注意:MethodType的ptypes需要与目标方法签名一致(不支持向上/向下转型,比如用Integer去获取入参为Number的方法是不允许的,同样用Number去获取入参为Integer的方法也不行。这个还能理解),rtype需要与目标方法的返回值一致(不允许向上/向下转型。不允许向下很容易理解,为何要不允许向上转型,有点不清楚) 创建好MethodType对象后,还可以对其进行修改,MethodType类中提供了一系列的修改方法,比如:changeParameterType、changeReturnType等。 MethodType的对象实例只能通过MethodType类中的静态工厂方法来创建,而且MethodType类的所有对象实例都是不可变的。如果修改了MethodType实例中的信息,就会生成另外一个MethodType实例。 Lookup:MethodHandle.Lookup相当于MethodHandle工厂类,通过findxxx方法可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。 invoke:在得到MethodHandle后就可以进行方法调用了,有三种调用形式: (1)invokeExact:调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配; (2)invoke:参数类型松散匹配,通过asType自动适配; (3)invokeWithArguments:直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。 2.使用 1.要使用method handle,首先需要得到Lookup。这是创造方法,构造函数,属性的method handles的工厂类。 2.要创建MethodHandle,lookup需要一个定义了它的类型的MethodType对象。这里的类型包括了传入参数的类型,和最后返回的类型,要一一对应。第一个是返回类型,如果没有返回值就是Void.class, 后面是可变的传入参数的类型。 a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a […]

反射在JVM层面的实现

转自 该篇文章,将从我们熟悉的Java反射API出发,一直到JVM源码对Java反射的支持。本文分析用的是JDK18。与原文的1.7相比,变化还是不少的,但总体思想没变 首先看使用的示例代码: 这段代码的逻辑十分简单,就是利用反射来获取一个字符串的哈希值。该示例毫无实际利用的价值,但是拿来举例子是足够的了。 这篇文章我们要弄明白的问题是: getDeclaredMethod方法是如何正确查找到方法的; invoke方法是如何被执行的; 这两个方法的调用,在虚拟机的层面上究竟发生了什么; JDK分析 反射的源码在JDK层面上是比较简单的,getDeclaredMethod方法的源码是: 其中最重要的就是 searchMethods是依据传入的方法名字和方法参数,从privateGetDeclaredMethods返回的Method[]中找到对应的方法,返回该方法的 副本。所以重要的逻辑是在privateGetDeclaredMethods中,该方法的源码是: 该方法大体上可以分成两个部分: 从本地缓存中获取Method[]; 如果本地没有缓存,则从虚拟机中获取; 本地缓存 先考察从本地缓存获取,其中关键的一句话是: 这一句话就是访问本地缓存。ReflectionData是Class的一个内部类,它里面存储了一个Class所含有的各色信息,包括字段、方法,并且进行了一定的分类,源码是: 这个源码要注意的是其构造函数,在这个构造函数中,只设置了redefinedCount,其余字段都会是Null。reflectionData()方法源码是: 这段代码有一个很关键的点:本地缓存的ReflectionData是使用SoftReference的,这意味着,在内存紧张的时候会被回收掉。如果在回收掉之后再次请求获得Method[],那么就会新建一个SoftReference,作为缓存。也就是其中newReflectionData的逻辑: 这段逻辑看上去很简单,但是其中有一些很容易遗漏的点。首先要注意的是,当走到 这一句的时候,rd里面只有一个redefinedCount字段被设置,其余字段都还是null。该方法的核心是: 该方法的底层是使用Unsafe的compareAndSwapObject方法来实现的: 这个方法的含义就是:如果clazz内存分布中,在reflectionDataOffset位置的地方,如果期望的值是oldData,那么就会使用newData来替换掉oldData。而在clazz内存的reflectionDataOffset的地方,恰好就是Class类中reflectionData域引用的地方。所以这个方法的真实含义,借助CAS原子操作,将老的reflectionData替换为新的reflectionData。 但是,这里要注意的一点是,此时的newData,也就是 它是一个新建的ReflectionData的实例的soft reference。而新建的ReflectionData,也就是rd,前面已经提到了,它只有redefinedCount字段是被设置好了的,其余字段都还是null。所以如果没有本地缓存,在方法返回了之后,一直到privateGetDeclaredMethods方法的 此刻的rd就是方才新建的ReflectionData,只有redefinedCount字段是设置的。所以判断: 是必然不成立的,因此最终会走到从JVM中获取Method[]的代码处。我们总结一下,在这种情况下,Class的实例类似于: 从JVM获取 前面已经提到,在本地缓存失效,或者被回收了之后,需要从JVM当中获得Method[]: 所以最后是通过调用native方法。下面是C++的环节了 从JVM中取到了Method[]。该方法在JVM中的头文件声明是: 该方法的实现是: 如我在其中注释的一样,整个过程可以分成几步: 先处理基本类型和数组类型; 确保类已经完成链接,在此处就是确保String已经完成了链接 统计符合要求的方法,并根据方法个数为结果分配内存 创建Method[]数组 下面我们将对2和4进行详细的分析。在这之前先对JVM的oop-klass做一个简单的介绍: oop: ordinary object pointer,普通对象指针,用于描述对象的实例信息 klass:Java类在JVM中的表示,是对Java类的描述,看名称是元数据对于我们日常说的一个对象来说,它们的oop-klass模型如图: JVM就是用这种方式,将一个对象的数据和对象模型进行分离。普遍意义上来说,我们说持有一个对象的引用,指的是图中的handle,它是oop的一个封装。 处理基本类型和数组类型 JNIHandles::resolve_non_null方法将ofClass转化为JVM内部的一个oop,因为JVM只会直接操作oop实例。顾名思义,is_primitive是判断是否属于基本类型,其源码实现是: 这一段的逻辑十分简单,取到java_class这个oop中在klass字段上的值,如果是Null则被认为是基本类型。因为对于任何一个非基本类型的对象来说,oop中必然包含着一个指向其klass实例的指针。 另外一个判断条件: java_lang_Class::as_klass方法将一个oop包装成一个klass,逻辑和前面的is_primitive十分相像: 在JVM中,对象在内存中的基本存在形式就是oop,正如前面的图所描述的那样。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass关系如图: 我们接下来看调用的 is_array_klass […]

Java-数组与List的转换

基本数据类型,在由数组转List时,不能用Arrays.asList等方式。具体看图,注意结果类型,需要boxed一下。非基本数据类型可以(比如引用数据类型String)。这个很容易忽略,然后若对Stream及FunctionalInterface不了解,看参数会一脸懵。 String[] strings4 = Arrays.stream(strings3).map(String::toUpperCase).toArray(String[]::new); // 这里toArray里可以传IntFunction<R> IntFunction<String[]> aNew = String[]::new; // 这里返回默认是Consumer<T>,但也可以是现在这种,这个有机会再了解了解 String[] apply = aNew.apply(10);

ASM 库的介绍和使用

转自这篇文章主要介绍 ASM 库的结构、主要的 API,并且通过两个示例说明如何通过 ASM 修改 .class 文件中的方法和属性。 另ASM相关,在JDK中也有,只是相关代码只开放给内部 以及Spring、aspectJ都有ASM相关的。 ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。 一. ASM 的结构 ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。ASM 库的结构如下所示: Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和 ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类. Tree:提供了 Java 字节码在内存中的表现 Commons:提供了一些常用的简化字节码生成、转换的类和适配器 Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用 XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化 […]

访问者模式和 ASM

转自 之前的文章介绍了 .class 文件的结构、JVM 对 .class 文件加载以及在 JVM 中是怎么执行程序的,接下来的文章会介绍 ASM 的使用,ASM 是运用访问者模式设计的,本篇文章就介绍一下访问者模式的概念以及其在 ASM 中的应用。 一. 概述 & 定义 定义:封装某些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些数据元素的新的操作 意图:主要将数据结构和数据操作分离 主要解决:稳定的数据结构和易变的操作的解耦 适用场景: 假如一个对象中存在着一些与本对象不相干(或者关系较弱)的操作,可以使用访问者模式把这些操作封装到访问者中去,这样便避免了这些不相干的操作污染这个对象。 假如一组对象中,存在着相似的操作,可以将这些相似的操作封装到访问者中去,这样便避免了出现大量重复的代码 访问者模式适用于对功能已经确定的项目进行重构的时候适用,因为功能已经确定,元素类的数据结构也基本不会变了;如果是一个新的正在开发中的项目,在访问者模式中,每一个元素类都有它对应的处理方法,每增加一个元素类都需要修改访问者类,修改起来相当麻烦。 二. 示例 如果老师教学反馈得分大于等于85分、学生成绩大于等于90分,则可以入选成绩优秀奖;如果老师论文数目大于8、学生论文数目大于2,则可以入选科研优秀奖。 在这个例子中,老师和学生就是Element,他们的数据结构稳定不变。从上面的描述中,我们发现,对数据结构的操作是多变的,一会儿评选成绩,一会儿评选科研,这样就适合使用访问者模式来分离数据结构和操作。 2.1 创建抽象元素 2.2 创建具体元素 创建两个具体元素 Student 和 Teacher,分别实现 Element 接口 2.3 创建抽象访问者 2.4 创建具体访问者 创建一个根据分数评比的具体访问者 GradeSelection,实现 Visitor 接口 2.5 访问者代码调用 上述代码即是一个简单的访问者模式的示例代码,输出如下所示: 上述代码可以分为三步:1. 创建一个元素类的对象2. 创建一个访问类的对象3. 元素对象通过 Element#accept(Visitor […]

lWoHvYe 无悔,专一