一 前语
服务功用是指服务在特定条件下的呼应速度、吞吐量和资源利用率等方面的表现。据核算,功用优化方面的精力投入,一般占软件开发周期的10%到25%左右,当然这和运用的性质和规划有关。功用对进步用户体验,确保体系可靠性,下降资源运用率,乃至增强市场竞争力等方面,都有着很大的影响。
功用优化是个体系性工程,宏观上可分为网络,服务,存储几个方向,每个方向又能够细分为架构,规划,代码,可用性,度量等多个子项。 本文将要点从代码和规划两个子项翻开,谈谈那些进步功用的知识点。当然,很多功用进步战略都是有代价的,适用于某些特定场景,咱们在学习和运用的时分,最好带着批评的思想,决策前,做好利害权衡。
先简略罗列一下功用优化方向:
二 代码优化
2.1 相关代码
相关代码优化是经过预加载相关代码,避免在运转时加载方针代码,造成运转时担负。咱们知道Java有两个类加载器:Bootstrap class loader和Application class loader。Bootstrap class loader负责加载Java API中包含的中心类,而Application class loader则负责加载自界说类。相关代码优化能够经过以下几种办法来完结。
预加载相关
预加载相关类是指在程序启动时预先加载方针与相关类,以避免在运转时加载。能够经过静态代码块来完结预加载,如下所示:
public class MainClass {
static {
// 预加载MyClass,其完结了相关功用
Class.forName("com.example.MyClass");
}
// 运转相关功用的代码
// ...
}
运用线程池
线程池能够让多个使命运用同一个线程池中的线程,然后削减线程的创立和毁掉本钱。运用线程池时,能够在程序启动时创立线程池,并在主线程中预加载相关代码。然后以异步办法运用线程池中的线程来履行相关代码,能够进步程序的功用。
运用静态变量
能够运用静态变量来缓存与相关代码有关的方针和数据。在程序启动时,能够预先加载相关代码,并将方针或数据存储在静态变量中。然后在程序运转时运用静态变量中缓存的方针或数据,以避免重复加载和生成。这种办法能够有效地进步程序的功用,但需求留意静态变量的运用,确保它们在多线程环境中的安全性。
2.2 缓存对齐
在介绍缓存对齐之前,需求先遍及一些CPU指令履行的相关知识。
- 缓存行(Cache line) : CPU读取内存数据时并非一次只读一个字节,一般是会读一段64字节(硬件决定)长度的接连的内存块(chunks of memory),这些块咱们称之为缓存行。
- 伪同享(False Sharing):当运转在两个不同CPU上的两个线程写入两个不同的变量时,假如这两个变量恰好存储在同一个 CPU 缓存行中,就会产生伪同享(False Sharing)。即当第一个线程修正缓存行中其间一个变量时,其他引证此缓存行变量的线程的缓存行将会无效。假如CPU需求读取失效的缓存行,它有必要等候缓存行刷新,这会导致功用下降。
- CPU中止运转(stall):当一个中心需求等候另一个中心从头加载缓存行时(呈现伪同享时),它无法持续履行下一条指令,只能中止运转等候,这被称之为stall。削减伪同享也就意味着削减了stall的产生。
- IPC(instructions per cycle):它标明均匀每个 CPU 周期履行的指令数量,很显然该数值越大功用越好。能够依据IPC方针(比方:阈值1.0)来简略判别程序是归于拜访密集型仍是核算密集型。Linux体系中能够经过tiptop指令来查看每个进程的CPU硬件数据:
怎么简略来区分访存密集型和核算密集型程序?
-
假如 IPC < 1.0, 很或许是 Memory stall 占主导,八成意味着访存密集型。
-
假如IPC > 1.0, 很或许是核算密集型的程序。
-
CPU利用率:是指体系中CPU处于繁忙状况的时刻与总时刻的份额。繁忙状况时刻又能够进一步拆分为指令(instruction)履行耗费周期cycle(%INS) 和 stalled 的周期cycle(%STL)。perf 采集了10秒内全部 CPU 的运转状况:
IPC核算
IPC = instructions/cycles
上图中,能够核算出成果为:0.79
现代处理器一般有多条流水线(比方:4中心),运转 perf 的那台机器,IPC的理论值可到达4.0。
假如咱们从 IPC的角度来看,这台机器只运转到其处理器最高速度的 19.7%(0.79 / 4.0)。
总归,经过Top指令,看到CPU运用率之后,能够进一步剖析指令履行耗费周期和 stalled 周期,有这些更详细的方针之后,就能够知道该怎么更好地对运用和体系进行调优。
- 缓存对齐: 是经过调整数据在内存中的散布,让数据在被缓存时,更有利于CPU从缓存中读取,然后避免了频繁的内存读取,进步了数据拜访的速度。
缓存填充(Padding)
削减伪同享也就意味着削减了stall的产生,其间一个手法便是经过填充(Padding)数据的办法,即在适当的间隔处刺进一些对齐的空间来填充缓存行,然后使每个线程的修正不会脏污同一个缓存行。
/**
* 缓存行填充测验
*
* @author liuhuiqing
* @date 2023年04月28日
*/
public class FalseSharingTest {
private static final int LOOP_NUM = 1000000000;
public static void main(String[] args) throws InterruptedException {
Struct struct = new Struct();
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < LOOP_NUM; i++) {
struct.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < LOOP_NUM; i++) {
struct.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("cost time [" + (System.currentTimeMillis() - start) + "] ms");
}
static class Struct {
// 同享变量
volatile long x;
// 一个long占用8个字节,此处界说7个填充数据,来确保事务数据x和y散布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 运用数组代替不会收效,考虑一下,为什么?
// 同享变量
volatile long y;
}
}
经过本地测验,这种以空间换时刻的办法,即完结了缓存行数据对齐的办法,在履行功率方面,比没有对齐之前,进步了5倍!
@Contended注解
在Java 8中,引入了@Contended注解,该注解能够用来告知JVM对字段进行缓存对齐(将字段放入不同的缓存行),然后进步程序的功用。运用@Contended注解时,需求在JVM启动时添加参数-XX:-RestrictContended,完结如下所示:
import sun.misc.Contended;
public class ContendedTest {
@Contended
volatile long a;
@Contended
volatile long b;
public static void main(String[] args) throws InterruptedException {
ContendedTest c = new ContendedTest();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000_0000L; i++) {
c.a = i;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000_0000L; i++) {
c.b = i;
}
});
final long start = System.nanoTime();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println((System.nanoTime() - start) / 100_0000);
}
}
对齐内存与本地变量
缓存填充是处理CPU伪同享问题的处理计划之一,在实践运用中,是否还有其它计划来处理这一问题呢?答案是有的:即对齐内存和本地变量。
- 对齐内存:内存行的巨细一般为64个字节,这个巨细是硬件决定的,但大多数编译器默认状况下都以4字节的边界对齐,经过将变量依照内存行的巨细对齐,能够避免伪同享问题;
- 本地变量:在不同线程之间运用不同的变量存储数据,避免不同的线程之间同享同一块内存,Java中的ThreadLocal便是一种典型的完结办法;
2.3 分支猜测
分支猜测是CPU动态履行技能中的主要内容,是经过猜测程序中的分支句子(如if-else句子或许循环句子)的履行途径来进步CPU履行功率的技能。其原理是依据之前的历史记录和核算数据,猜测程序下一步要履行的指令是分支跳转指令仍是次序履行指令,然后提早加载相关数据,削减CPU等候指令履行的闲暇时刻。猜测准确率越高,CPU的功用进步就越高。那么怎么进步猜测的准确率呢?
- 重视圈杂乱度
过多的条件句子和嵌套的条件句子会导致分支的猜测难度大幅上升,然后下降分支猜测的准确率和功率。一般来说,能够经过优化代码逻辑结构、削减冗余等办法来避免过多的条件句子和嵌套的条件句子。
- 优先处理常用途径
在编写代码时,应该优先处理常用途径,以削减CPU对分支的猜测,进步猜测准确率和功率。例如,在if-else句子中,应该将常用的途径放在if句子中,而将不常用的途径放在else句子中。
2.4 写时仿制
Copy-On-Write (COW)是一种内存管理机制,也被称为写时仿制。其主要思想是在需求写入数据时,先进行数据拷贝,然后再进行操作,然后避免了对数据进行不必要的仿制和操作。COW机制能够有效地下降内存运用率,进步程序的功用。
在创立进程或线程的时分,操作体系为其分配内存时,不是仿制一个完好的物理地址空间,而是创立一个指向父进程/线程物理地址空间的虚拟地址空间,并为它们的一切页面设置”只读”标志。当子进程/线程需求修正页面时,会触发一个缺页反常,并将涉及到的页面进行数据的仿制,并为仿制的页面从头分配内存。子进程/线程只能够操作仿制后的地址空间,父进程/线程的原始内存空间则被保存。
由于COW机制在写入之前进行数据拷贝,所以能够有效地避免频繁的内存拷贝和分配操作,下降了内存的占用率,进步了程序的功用。而且,COW机制也避免了数据的不必要仿制,然后削减了内存的耗费和内存碎片的产生,进步了体系中可用内存的数量。
ArrayList类能够运用Copy-On-Write机制来进步功用。
// 初始化数组
private List<String> list = new CopyOnWriteArrayList<>();
// 向数组中添加元素
list.add("value");
需求留意的是,Copy-On-Write机制适用于读操作比写操作多的状况,由于它假定写操作的频率较低,然后能够经过献身仿制的开支来削减锁的操作和内存分配的耗费。
2.5 内联优化
在Java中,每次调用办法都需求进行一些额定的操作,例如创立仓库帧、保存寄存器状况等,这些额定的操作会耗费必定的时刻和内存资源。内联优化是一种编译器优化技能,Java虚拟机一般运用即时编译器(JIT)来进行办法内联,用于进步程序的功用。内联优化的方针是将函数的调用替换成函数自身的代码,以削减函数调用的开支,然后进步程序的运转功率。
需求留意的是,办法内联并不是在一切状况下都能够进步程序的运转功率。假如办法内联导致代码杂乱度添加或许内存占用添加,反而会下降程序的功用。因而,在运用办法内联时需求依据具体状况进行权衡和优化。
final润饰符
final润饰符能够使办法成为不行重写的办法。由于不行重写,所以在编译器优化时能够将它们的代码嵌入到调用它们的代码中,然后避免函数调用的开支。运用final润饰符能够在必定程度上进步程序的功用,但一起也削弱了代码的可扩展性。
限制办法长度
办法的长度会影响其在编译时能否被内联。一般状况下,长度较小的办法更简略被内联。因而,能够在规划中将代码分化和重构为更小的函数。这种办法并不是100%确保能够内联,但至少进步了完结此优化的机会。内联调优参数,如下表格:
JVM参数 | 默认值 (JDK 8, Linux x86_64) | 参数阐明 |
---|---|---|
-XX:MaxInlineSize= | 35 字节码 | 内联办法巨细上限 |
-XX:FreqInlineSize= | 325 字节码 | 内联热办法的最大值 |
-XX:InlineSmallCode= | 1000字节的原生代码(非分层) 2000字节的原生代码(分层编译) | 假如终究一层的的分层编译代码量已经超过这个值,就不进行内联编译 |
-XX:MaxInlineLevel= | 9 | 调用层级比这个值深的话,就不进行内联 |
内联注解
在Java 5之后,引入了内联注解@inline,运用此注解能够在编译时通知编译器,将该办法内联到它的调用途。注解@inline在Java 9之后已经被弃用,能够运用@ForceInline注释来代替,一起设置JVM参数:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+JVMCICompiler
@ForceInline
public static int add(int a, int b) {
return a + b;
}
2.6 编码优化
反射机制
Java反射在必定程度上会影响功用,由于它需求在运转时进行类型查看转换和办法查找,这比直接调用办法会更耗时。此外,反射也不会遭到编译器的优化,因而或许会导致更慢的代码履行速度。
要处理这个问题有以下几种办法:
- 尽或许运用原生办法调用,而不是经过反射调用;
- 尽或许缓存反射调用成果,避免重复调用。例如,能够将反射成果缓存到静态变量中,以便下次运用时直接获取,而不必再次运用反射;
- 运用字节码增强技能;
下面着重介绍一下反射成果缓存和字节码增强两种计划。
- 反射成果缓存能够大幅削减反射进程中的类型查看,类型转换和办法查找等动作,是下降反射对程序履行功率影响的一种优化战略。
/**
* 反射工具类
*
* @author liuhuiqing
* @date 2023年5月7日
*/
public abstract class BeanUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(BeanUtils.class);
private static final Field[] NO_FIELDS = {};
private static final Map<Class<?>, Field[]> DECLARED_FIELDS_CACHE = new ConcurrentReferenceHashMap<Class<?>, Field[]>(256);
private static final Map<Class<?>, Field[]> FIELDS_CACHE = new ConcurrentReferenceHashMap<Class<?>, Field[]>(256);
/**
* 获取当时类及其父类的特点数组
*
* @param clazz
* @return
*/
public static Field[] getFields(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Class must not be null");
}
Field[] result = FIELDS_CACHE.get(clazz);
if (result == null) {
Field[] fields = NO_FIELDS;
Class<?> searchType = clazz;
while (Object.class != searchType && searchType != null) {
Field[] tempFields = getDeclaredFields(searchType);
fields = mergeArray(fields, tempFields);
searchType = searchType.getSuperclass();
}
result = fields;
FIELDS_CACHE.put(clazz, (result.length == 0 ? NO_FIELDS : result));
}
return result;
}
/**
* 获取当时类特点数组(不包含父类的特点)
*
* @param clazz
* @return
*/
public static Field[] getDeclaredFields(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Class must not be null");
}
Field[] result = DECLARED_FIELDS_CACHE.get(clazz);
if (result == null) {
result = clazz.getDeclaredFields();
DECLARED_FIELDS_CACHE.put(clazz, (result.length == 0 ? NO_FIELDS : result));
}
return result;
}
/**
* 数组兼并
*
* @param array1
* @param array2
* @param <T>
* @return
*/
public static <T> T[] mergeArray(final T[] array1, final T... array2) {
if (array1 == null || array1.length < 1) {
return array2;
}
if (array2 == null || array2.length < 1) {
return array1;
}
Class<?> compType = array1.getClass().getComponentType();
int newArrLength = array1.length + array2.length;
T[] newArr = (T[]) Array.newInstance(compType, newArrLength);
int firstArrayLen = array1.length;
System.arraycopy(array1, 0, newArr, 0, firstArrayLen);
try {
System.arraycopy(array2, 0, newArr, firstArrayLen, array2.length);
} catch (ArrayStoreException ase) {
final Class<?> type2 = array2.getClass().getComponentType();
if (!compType.isAssignableFrom(type2)) {
throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of "
+ compType.getName(), ase);
}
throw ase;
}
return newArr;
}
}
- 字节码增强技能,一般运用第三方库来完结,例如Javassist或Byte Buddy,在运转时生成字节码,然后避免运用反射。
为什么动态字节码生成办法相比反射也能够进步履行功率呢?
- 动态字节码生成的办法在编译期就已经将类型信息确定下来,无需进行类型查看和转换;
- 动态字节码生成的办法能够直接调用办法,无需查找,进步了履行功率;
- 动态字节码生成的办法只需求在生成字节码时获取一次Method方针,多次调用时能够直接运用,避免了重复获取Method方针的开支;
这儿就不再举例阐明了,感兴趣的同学能够自行查阅资料进行深化学习。
反常处理
有效的处理反常能够确保程序的稳定性和可靠性。但反常的处理对功用仍是有必定的影响的,这一点常常被人忽视。影响功用的具体表现为:
- 呼应推迟:当反常被抛出时,Java虚拟机需求查找并履行相应的反常处理程序,这会导致必定的推迟。假如程序中存在很多的反常处理,这些推迟或许会累积,导致程序的全体功用下降。
- 内存占用:反常处理需求在仓库中创立反常方针,这些方针需求占用内存。假如程序中存在很多的反常处理,这些反常方针或许会占用很多的内存,导致程序的全体内存占用量添加。
- CPU占用:反常处理需求履行额定的代码,这会导致CPU占用率添加。假如程序中存在很多的反常处理,这些额定的代码或许会导致CPU占用率过高,导致程序的全体功用下降。
一些基准测验显现,反常处理或许会导致程序的功用下降几个百分点。在Java虚拟机标准中说到,在没有反常产生的状况下,依据仓库的办法调用或许比依据反常的办法调用快2-3倍。此外,一些实验标明,在反常处理程序中运用很多的try-catch句子,或许会导致功用下降10倍以上。
为避免这些问题,在编写代码时谨慎地运用反常处理机制,并确保对反常进行适当的记录和报告,避免过度运用反常处理机制。
日志处理
先看以下代码:
LOGGER.info("result:" + JsonUtil.write2JsonStr(contextAdContains) + ", logid = " + DigitThreadLocal.getLogId());
以上示例代码中,相似的日志打印办法很常见,难道有什么问题吗?
- 功用问题:每次运用+进行字符串拼接时,都会创立一个新的字符串方针,这或许会导致内存分配和废物收回的开支添加;
- 可读性问题:运用+进行字符串拼接时,代码或许会变得难以阅读和了解,特别是在需求衔接多个字符串时;
- 假如日志等级调整到ERROR形式,咱们希望日志的字符串内容不需求进行加工核算,但这种写法,即便日志处于不需求打印的形式,日志内容也进行了无效核算;
特别实在恳求量和日志打印量比较高的场景下,日志内容的序列化和写文件操作,对服务的耗时影响能够到达10%,乃至更多。
暂时方针
暂时方针一般是指在办法内部创立的方针。很多创立暂时方针会导致Java虚拟机频繁进行废物收回,然后影响程序的功用。也会占用很多的内存空间,然后导致程序崩溃或许呈现内存走漏等问题。
为了避免很多创立暂时方针,在编码时,能够采取以下办法:
- 字符串拼接中,运用StringBuilder或StringBuffer进行字符串拼接,避免运用衔接符,每次都创立新的字符串方针;
- 在调集操作中,尽量运用批量操作,如addAll、removeAll等,避免频繁的add、remove操作,触发数组的扩容或许缩容;
- 在正则表达式中,能够运用Pattern.compile()办法预编译正则表达式,避免每次都创立新的Matcher方针;
- 尽量运用基本数据类型,避免运用包装类,由于包装类的创立和毁掉都会产生暂时方针;
- 尽量运用方针池的办法创立和管理方针,比方运用静态工厂办法创立方针,避免运用new关键字创立方针,由于静态工厂办法能够重用方针,避免创立新的暂时方针;
暂时方针的生命周期应该尽或许短,以便及时开释内存资源。暂时方针的生命周期过长一般是由以下原因引起的:
- 方针未被正确地开释:假如在办法履行结束后,暂时方针没有被正确地开释,就会导致内存走漏危险;
- 方针过度同享:假如暂时方针被过度同享,就或许会导致多个线程一起拜访同一个方针,然后导致线程安全问题和功用问题;
- 方针创立过于频繁:假如在办法内部频繁地创立暂时方针,就会导致内存开支过大,或许会引起功用乃至内存溢出问题;
为避免暂时方针的生命周期过长,主张采取以下办法:
- 及时开释方针:在办法履行结束后,应该及时开释暂时方针(比方主动将方针设置为null),以便收回内存资源;
- 避免过度同享:在多线程环境下,应该避免过度同享暂时方针,能够运用局部变量或ThreadLocal等办法来避免同享问题;
- 方针池技能:运用方针池技能能够避免频繁创立暂时方针,然后下降内存开支。方针池能够预先创立必定数量的方针,并在需求时从池中获取方针,运用结束后再将方针放回池中;
小结
正所谓:“不积跬步,无以致千里;不积小流,无以成江海”。以上罗列的编码细节,都会直接或间接的影响服务的履行功率,仅仅影响多少的问题。现实中,有时分咱们不必过于苛求,但它们有一个共同的注脚:极客精力。
三 规划优化
3.1 缓存
合理运用缓存能够有效进步运用程序的功用,缩短数据拜访时刻,下降对数据源的依赖性。缓存能够进行多层级的规划,举例,为了进步运转功率,CPU就规划了L1-L3三级缓存。在运用规划的时分,咱们也能够依照事务诉求进行层规划。常见的分层规划有本地缓存(L1),长途散布式缓存(L2)两级。
本地缓存能够削减网络恳求、节约核算资源、削减高负载数据源拜访等优势,从而进步运用程序的呼应速度和吞吐量。常见的本地缓存中间件有:Caffeine、Guava Cache、Ehcache。当然你也能够在运用相似Map容器,在运用程序中构建自己的缓存结构。 散布式缓存相比本地缓存的优势是能够确保数据一致性、只保存一份数据,削减数据冗余、能够完结数据分片,完结大容量数据的存储。常见的散布式缓存有:Redis、Memcached。
完结一个简略的LRU本地缓存示例如下:
/**
* Least recently used 内存缓存过期战略:最近最少运用
* Title: 带容量的<b>线程不安全的</b>最近拜访排序的Hashmap
* Description: 终究拜访的元素在终究面。<br>
* 假如要线程安全,请运用<pre>Collections.synchronizedMap(new LRUHashMap(123));</pre> <br>
*
* @author: liuhuiqing
* @date: 20123/4/27
*/
public class LRUHashMap<K, V> extends LinkedHashMap<K, V> {
/**
* The Size.
*/
private final int maxSize;
/**
* 初始化一个最大值, 按拜访次序排序
*
* @param maxSize the max size
*/
public LRUHashMap(int maxSize) {
//0.75是默认值,true标明按拜访次序排序
super(maxSize, 0.75f, true);
this.maxSize = maxSize;
}
/**
* 初始化一个最大值, 按指定次序排序
*
* @param maxSize 最大值
* @param accessOrder true标明按拜访次序排序,false为刺进次序
*/
public LRUHashMap(int maxSize, boolean accessOrder) {
//0.75是默认值,true标明按拜访次序排序,false为刺进次序
super(maxSize, 0.75f, accessOrder);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > maxSize;
}
}
3.2 异步
异步能够进步程序的功用和呼应才能,使其能更高效地处理大规划数据或并发恳求。其底层原理涉及到操作体系的多线程、事件循环、使命队列以及回调函数等关键技能,除此之外,异步的思想在运用架构规划方面也有广泛的运用。惯例的多线程,音讯队列,呼应式编程等异步处理计划这儿就不再翻开介绍了,这儿介绍两个咱们或许简略忽视但有用技能:非堵塞IO和 协程。
非堵塞IO
Java Servlet 3.0标准中引入了异步Servlet的概念,能够协助开发者进步运用程序的功用和并发处理才能,其原理是非堵塞IO运用单线程一起处理多个恳求,避免了线程切换和堵塞的开支,特别是在读取大文件或许进行杂乱耗时核算场景时,能够避免堵塞其他恳求的处理。Spring MVC框架中也供给了相应的异步处理计划。
•运用Callable办法完结异步处理
@GetMapping("/async/callable")
public WebAsyncTask<String> asyncCallable() {
Callable<String> callable = () -> {
// 履行异步操作
return "异步使命已完结";
};
return new WebAsyncTask<>(10000, callable);
}
•运用DeferredResult办法完结异步处理
@GetMapping("/async/deferredresult")
public DeferredResult<String> asyncDeferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>(10000L);
// 异步处理完结后设置成果
deferredResult.setResult("DeferredResult异步使命已完结");
return deferredResult;
}
协程
咱们知道线程的创立、毁掉都非常耗费体系资源,所以有了线程池,但这还不行,由于线程的数量是有限的(千等级),线程会堵塞操作体系线程,无法尽或许的进步吞吐量。由于运用线程的本钱很高,所以才会有了虚拟线程,它是用户态线程,本钱是相当低廉的,调度也完全由用户进行操控(JDK 中的调度器),它相同能够进行堵塞,但不必堵塞操作体系线程,充分进步了硬件利用率,高并发也上了一个量级。
很长一段时刻,协程概念并非作为JVM内置的功用,而是经过第三方库或框架完结的。目前比较常用的协程完结库有Quasar、Kilim等。但在Java19版本中,引入了虚拟线程(Virtual Threads )的支撑(处于Preview阶段)。
虚拟线程是java.lang.Thread的一个完结,能够运用java.lang.Thread.Builder接口创立
Thread thread = Thread.ofVirtual()
.name("Virtual Threads")
.unstarted(runnable);
也能够经过一个线程工厂类进行创立:
ThreadFactory factory = Thread.ofVirtual().factory();
虚拟线程运转的载体有必要是线程,同一个线程中能够运转多个虚拟线程实例。
3.3 并行
并行处理的思想在大数据,多使命,流水线处理,模型训练等各个方面发挥着重要作用,包含前面介绍的异步(多线程,协程,音讯等),也是树立在并行的基础上。在运用层面,典型的场景有:
- 散布式核算框架中的MapReduce便是选用一种分而治之的思想规划出来的,将杂乱或核算量大的使命,切分成一个个小的使命,小使命分别在不同的线程或服务器上并行的履行,终究再汇总每个小使命的成果。
- 边际核算(Edge Computing)是一种散布式核算范式,它将核算、存储和网络服务的部分功用从云数据中心延伸至离数据源更近的当地,即网络的边际。这种核算办法能够完结低推迟、节约带宽、进步数据安全性以及实时处理与剖析等优势。
在代码完结方面,做好解耦规划,接下来就能够进行并行规划了,比方:
- 多个恳求能够经过多线程并行处理,每个恳求的不同处理阶段;
- 如查询阶段,能够选用协程并行履行;
- 存储阶段,能够选用音讯订阅发布的办法进行处理;
- 监控核算阶段,就能够选用NIO异步的办法进行方针数据文件的写入;
- 恳求/呼应选用非堵塞IO形式;
3.4 池化
池化便是初始预设资源,下降每次获取资源的耗费,如创立线程的开支,获取长途衔接的开支等。典型的场景便是线程池,数据库衔接池,事务处理成果缓存池等。
以数据库衔接池为例,其本质是一个 socket 的衔接。为每个恳求翻开和保护数据库衔接,尤其是动态数据库驱动的运用程序的恳求,既昂贵又浪费资源。为什么这么说呢?以MySQL数据库树立衔接(TCP协议)为例,树立衔接一共分三步:
- 树立TCP衔接,经过三次握手完结;
- 服务器发送给客户端「握手信息」,客户端呼应该握手音讯;
- 客户端「发送认证包」,用于用户验证,验证成功后,服务器返回OK呼应,之后开始履行指令;
简略粗略核算,完结一次数据库衔接,客户端和服务器之间需求至少往返7次,总计均匀耗时大约在200ms左右,这对于很对C端服务来说,几乎是不能承受的。
落实到代码编写层面,也能够借助这一思想来优化咱们的程序履行功用。
- 公用的数据能够大局只界说一份,比方运用枚举,static润饰的容器方针等;
- 依据实践状况,提早设置List,Map等容器方针的初始化容量巨细,避免后面的扩容,对功用的影响;
- 亨元规划形式的运用等;
3.5 预处理
一般需求池化的内容,都是需求预处理的,比方为了确保服务的稳定性,线程池和数据库衔接池等需求池化的内容在JVM容器启动时,处理真实恳求之前,对这些池化内容进行预处理,比及真实的事务处理恳求过来时,能够正常的快速处理。除此之外,预处理还能够体现在体系架构层面。
- 为了进步呼应功用,将部分事务数据提早预加载到内存中;
- 为了减轻CPU压力,将核算逻辑提早履行,直接将核算后的成果数据保存下来,直接供调用方运用;
- 为了下降网络带宽本钱,将传输数据经过紧缩算法进行紧缩处理,到了方针服务,在进行解压,取得原始数据;
- Myibatis为了进步SQL句子的安全性和履行功率,也引入了预处理的概念;
四 总结
功用优化是程序开发进程中绕不过去一个课题,本文聚焦代码和规划两个方面,从CPU硬件到JVM容器,从缓存规划到数据预处理,全面的展示了功用优化的施行方向和落地细节。阐述的进程没有追求各个方向的面面俱到,但都给到了一些场景化案例,来辅佐了解和考虑,起到抛砖引玉的作用。
作者:京东零售 刘慧卿
内容来历:京东云开发者社区