共计 13849 个字符,预计需要花费 35 分钟才能阅读完成。
一 前言
服务性能是指服务在特定条件下的响应速度、吞吐量和资源利用率等方面的体现。据统计,性能优化方面的精力投入,通常占软件开发周期的 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=<n> | 35 字节码 | 内联办法大小下限 |
-XX:FreqInlineSize=<n> | 325 字节码 | 内联热办法的最大值 |
-XX:InlineSmallCode=<n> | 1000 字节的原生代码(非分层)2000 字节的原生代码(分层编译) | 如果最初一层的的分层编译代码量曾经超过这个值,就不进行内联编译 |
-XX:MaxInlineLevel=<n> | 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 容器,从缓存设计到数据预处理,全面的展示了性能优化的施行方向和落地细节。论述的过程没有谋求各个方向的八面玲珑,但都给到了一些场景化案例,来辅助了解和思考,起到抛砖引玉的成果。
作者:京东批发 刘慧卿
内容起源:京东云开发者社区