性能对提高用户体验,保证系统可靠性,降低资源使用率,甚至增强市场竞争力等方面,都有着很大的影响,性能优化 是程序开发过程中绕不过去一个课题。本文聚焦代码和设计两个方面,从CPU硬件到JVM容器,从缓存 设计到数据预处理,全面的展现了性能优化的实施方向和落地细节,希望能启发您的思考,为您带来帮助。
前言
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
服务性能是指服务在特定条件下的响应速度、吞吐量和资源利用率等方面的表现。据统计,性能优化方面的精力投入,通常占软件开发周期的10%到25%左右,当然这和应用的性质和规模有关。性能对提高用户体验,保证系统可靠性,降低资源使用率,甚至增强市场竞争力等方面,都有着很大的影响。
性能优化是个系统性工程,宏观上可分为网络,服务,存储几个方向,每个方向又可以细分为架构、设计、代码、可用性、度量等多个子项。本文将重点从代码和设计两个子项展开,谈谈那些提升性能的知识点。当然,很多性能提升策略都是有代价的,适用于某些特定场景,大家在学习和使用的时候,最好带着批判的思维,决策前,做好利弊权衡。
图1.性能优化方向罗列
代码优化
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
关联代码优化是通过预加载相关代码,避免在运行时加载目标代码,造成运行时负担。Java 有两个类加载器:Bootstrap class loader和Application class loader。Bootstrap class loader负责加载Java API中包含的核心类,而Application class loader则负责加载自定义类。关联代码优化可以通过以下几种方式来实现。
预加载关联类
预加载关联类是指在程序启动时预先加载目标与关联类,以避免在运行时加载。可以通过静态代码块来实现预加载,如下所示:
Class . forName ( "com.example.MyClass" ) ;
public class MainClass {
static {
// 预加载MyClass,其实现了相关功能
Class.forName("com.example.MyClass");
}
// 运行相关功能的代码
// ...
}
public class MainClass {
static {
// 预加载MyClass,其实现了相关功能
Class.forName("com.example.MyClass");
}
// 运行相关功能的代码
// ...
}
使用线程池
线程池可以让多个任务使用同一个线程池中的线程,从而减少线程的创建和销毁成本。使用线程池时,可以在程序启动时创建线程池,并在主线程中预加载相关代码。然后以异步 方式使用线程池中的线程来执行相关代码,可以提高程序的性能。
使用静态变量
可以使用静态变量来缓存与关联代码有关的对象和数据。在程序启动时,可以预先加载关联代码,并将对象或数据存储在静态变量中。然后在程序运行时使用静态变量中缓存的对象或数据,以避免重复加载和生成。这种方式可以有效地提高程序的性能,但需要注意静态变量的使用,确保它们在多线程环境中的安全性。
在介绍缓存对齐之前,需要先普及一些CPU指令执行的相关知识。CPU缓存与内存的逻辑关系如下图:
图2. CPU缓存与内存的逻辑关系
L1、L2都是CPU核心独享的缓存,L3为单个插槽上所有cpu共享的缓存,主存为所有cpu共享。根据局部性原理,CPU每次访问主存时都会读取至少一个缓存行的数据(通常一个缓存行为64字节,哪怕读取4字节数据,也会连续读取该数据之后的60字节)。
CPU不同缓存执行耗时
图3. CPU不同缓存执行耗时
图4. Linux系统中可以通过tiptop命令来查看每个进程的CPU硬件数据
如何简单来区分访存密集型和计算密集型程序?
如果 IPC < 1.0, 很可能是 Memory stall 占主导,多半意味着访存密集型。
如果IPC > 1.0, 很可能是计算密集型的程序。
图5. perf 采集了10秒内全部 CPU 的运行状态
IPC = instructions/cycles
现代处理器一般有多条流水线(比如: 4 核心),运行 perf 的那台机器,IPC的理论值可达到 4.0 。
如果我们从 IPC的角度来看,这台机器只运行到其处理器最高速度的 19.5 %( 0.78 / 4.0 ) 。
IPC计算
IPC = instructions/cycles
上图中,可以计算出结果为:0.78
现代处理器一般有多条流水线(比如:4核心),运行 perf 的那台机器,IPC的理论值可达到4.0。
如果我们从 IPC的角度来看,这台机器只运行到其处理器最高速度的 19.5%(0.78 / 4.0)。
IPC计算
IPC = instructions/cycles
上图中,可以计算出结果为:0.78
现代处理器一般有多条流水线(比如:4核心),运行 perf 的那台机器,IPC的理论值可达到4.0。
如果我们从 IPC的角度来看,这台机器只运行到其处理器最高速度的 19.5%(0.78 / 4.0)。
总之,通过Top命令,看到CPU使用率之后,可以进一步分析指令执行消耗周期和 stalled 周期,有这些更详细的指标之后,就能够知道该如何更好地对应用和系统进行调优。
缓存对齐 :是通过调整数据在内存中的分布,让数据在被缓存时,更有利于CPU从缓存中读取,从而避免了频繁的内存读取,提高了数据访问的速度。
缓存填充 (Padding)
减少伪共享也就意味着减少了stall的发生,其中一个手段就是通过填充(Padding)数据的形式,即在适当的距离处插入一些对齐的空间来填充缓存行,从而使每个线程的修改不会脏污同一个缓存行。
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++ ) {
Thread t2 = new Thread (() - > {
for ( int i = 0 ; i < LOOP_NUM; i++ ) {
System. out . println ( "cost time [" + ( System. currentTimeMillis () - start ) + "] ms" ) ;
// 共享变量,volatile设置内存可见性及内存屏障,禁止指令重排
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
/**
* 缓存行填充测试
*
* @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设置内存可见性及内存屏障,禁止指令重排
volatile long x;
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
// 共享变量
volatile long y;
}
}
/**
* 缓存行填充测试
*
* @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设置内存可见性及内存屏障,禁止指令重排
volatile long x;
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
// 共享变量
volatile long y;
}
}
@Contended注解
在Java 8中,引入了@Contended注解,该注解可以用来告诉JVM对字段进行缓存对齐(将字段放入不同的缓存行),从而提高程序的性能。使用@Contended注解时,需要在JVM启动时添加参数-XX:-RestrictContended,实现如下所示:
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++ ) {
Thread t2 = new Thread (() - > {
for ( int i = 0 ; i < LOOP_NUM; i++ ) {
System. out . println ( "cost time [" + ( System. currentTimeMillis () - start ) + "] ms" ) ;
// 共享变量,volatile设置内存可见性及内存屏障,禁止指令重排
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
/**
* 缓存行填充测试
*
* @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设置内存可见性及内存屏障,禁止指令重排
volatile long x;
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
// 共享变量
volatile long y;
}
}
/**
* 缓存行填充测试
*
* @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设置内存可见性及内存屏障,禁止指令重排
volatile long x;
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
// 共享变量
volatile long y;
}
}
缓存填充是解决CPU伪共享问题的解决方案之一,在实际应用中,是否还有其它方案来解决这一问题呢?答案是有的:即对齐内存和本地变量。
对齐内存:内存行的大小一般为64个字节,这个大小是硬件决定的,但大多数编译器默认情况下都以4字节的边界对齐,通过将变量按照内存行的大小对齐,可以避免伪共享问题;
本地变量:在不同线程之间使用不同的变量存储数据,避免不同的线程之间共享同一块内存,Java中的ThreadLocal就是一种典型的实现方式。
分支预测是CPU动态执行技术中的主要内容,是通过猜测程序中的分支语句(如if-else语句或者循环语句)的执行路径来提高CPU执行效率的技术。其原理是根据之前的历史记录和统计数据,预测程序下一步要执行的指令是分支跳转指令还是顺序执行指令,从而提前加载相关数据,减少CPU等待指令执行的空闲时间。预测准确率越高,CPU的性能提升就越高。那么如何提高预测的准确率呢?
过多的条件语句和嵌套的条件语句会导致分支的预测难度大幅上升,从而降低分支预测的准确率和效率。一般来说,可以通过优化代码逻辑结构、减少冗余等方式来避免过多的条件语句和嵌套的条件语句。
在编写代码时,应该优先处理常用路径,以减少CPU对分支的预测,提高预测准确率和效率。例如,在if-else语句中,应该将常用的路径放在if语句中,而将不常用的路径放在else语句中。
Copy-On-Write (COW)是一种内存管理机制,也被称为写时复制。其主要思想是在需要写入数据时,先进行数据拷贝,然后再进行操作,从而避免了对数据进行不必要的复制和操作。COW机制可以有效地降低内存使用率,提高程序的性能。
在创建进程或线程的时候,操作系统为其分配内存时,不是复制一个完整的物理地址空间,而是创建一个指向父进程/线程物理地址空间的虚拟地址空间,并为它们的所有页面设置"只读"标志。当子进程/线程需要修改页面时,会触发一个缺页异常,并将涉及到的页面进行数据的复制,并为复制的页面重新分配内存。子进程/线程只能够操作复制后的地址空间,父进程/线程的原始内存空间则被保留。
由于COW机制在写入之前进行数据拷贝,所以可以有效地避免频繁的内存拷贝和分配操作,降低了内存的占用率,提高了程序的性能。并且,COW机制也避免了数据的不必要复制,从而减少了内存的消耗和内存碎片的产生,提高了系统中可用内存的数量。
ArrayList类可以使用Copy-On-Write机制来提高性能。
private List < String > list = new CopyOnWriteArrayList <>() ;
// 初始化数组
private List<String> list = new CopyOnWriteArrayList<>();
// 向数组中添加元素
list.add("value");
// 初始化数组
private List<String> list = new CopyOnWriteArrayList<>();
// 向数组中添加元素
list.add("value");
需要注意的是,Copy-On-Write机制适用于读操作比写操作多的情况,因为它假定写操作的频率较低,从而可以通过牺牲复制的开销来减少锁的操作和内存分配的消耗。
2.5 内联优化
在Java中,每次调用方法都需要进行一些额外的操作,例如创建堆栈帧、保存寄存器状态等,这些额外的操作会消耗一定的时间和内存资源。内联优化是一种编译器优化技术,Java虚拟机通常使用即时编译器(JIT)来进行方法内联,用于提高程序的性能。内联优化的目标是将函数的调用替换成函数本身的代码,以减少函数调用的开销,从而提高程序的运行效率。
需要注意的是,方法内联并不是在所有情况下都能够提高程序的运行效率。如果方法内联导致代码复杂度增加或者内存占用增加,反而会降低程序的性能。因此,在使用方法内联时需要根据具体情况进行权衡和优化。
final修饰符
final修饰符可以使方法成为不可重写的方法。因为不可重写,所以在编译器优化时可以将它们的代码嵌入到调用它们的代码中,从而避免函数调用的开销。使用final修饰符可以在一定程度上提高程序的性能,但同时也减弱了代码的可扩展性。
限制方法长度
方法的长度会影响其在编译时能否被内联。通常情况下,长度较小的方法更容易被内联。因此,可以在设计中将代码分解和重构为更小的函数。这种方式并不是100%确保可以内联,但至少提高了实现此优化的机会。内联调优参数,如下表格:
内联注解
在Java 5之后,引入了内联注解@inline,使用此注解可以在编译时通知编译器,将该方法内联到它的调用处。注解@inline在Java 9之后已经被弃用,可以使用@ForceInline注释来替代,同时设置JVM参数:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+JVMCICompiler
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+JVMCICompiler
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+JVMCICompiler
public static int add ( int a, int b ) {
@ForceInline
public static int add(int a, int b) {
return a + b;
}
@ForceInline
public static int add(int a, int b) {
return a + b;
}
反射机制
Java反射在一定程度上会影响性能,因为它需要在运行时进行类型检查转换和方法查找,这比直接调用方法会更耗时。此外,反射也不会受到编译器的优化,因此可能会导致更慢的代码执行速度。
尽可能使用原生方法调用,而不是通过反射调用;
尽可能缓存反射调用结果,避免重复调用。例如,可以将反射结果缓存到静态变量中,以便下次使用时直接获取,而不必再次使用反射;
下面着重介绍一下反射结果缓存和字节码增强两种方案。
反射结果缓存可以大幅减少反射过程中的类型检查,类型转换和方法查找等动作,是降低反射对程序执行效率影响的一种优化策略。
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 ) ;
public static Field [] getFields ( Class < ? > clazz ) {
throw new IllegalArgumentException ( "Class must not be null" ) ;
Field [] result = FIELDS_CACHE. get ( clazz ) ;
Field [] fields = NO_FIELDS;
Class < ? > searchType = clazz;
while ( Object. class != searchType && searchType != null ) {
Field [] tempFields = getDeclaredFields ( searchType ) ;
fields = mergeArray ( fields, tempFields ) ;
searchType = searchType. getSuperclass () ;
FIELDS_CACHE. put ( clazz, ( result. length == 0 ? NO_FIELDS : result )) ;
public static Field [] getDeclaredFields ( Class < ? > clazz ) {
throw new IllegalArgumentException ( "Class must not be null" ) ;
Field [] result = DECLARED_FIELDS_CACHE. get ( clazz ) ;
result = clazz. getDeclaredFields () ;
DECLARED_FIELDS_CACHE. put ( clazz, ( result. length == 0 ? NO_FIELDS : result )) ;
public static < T > T [] mergeArray ( final T [] array1, final T... array2 ) {
if ( array1 == null || array1. length < 1 ) {
if ( array2 == null || array2. length < 1 ) {
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 ) ;
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 ) ;
/**
* 反射工具类
*
* @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;
}
}
/**
* 反射工具类
*
* @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;
}
}
为什么动态字节码生成方式相比反射也可以提高执行效率呢?
动态字节码生成的方式在编译期就已经将类型信息确定下来,无需进行类型检查和转换;
动态字节码生成的方式可以直接调用方法,无需查找,提高了执行效率;
动态字节码生成的方式只需要在生成字节码时获取一次Method对象,多次调用时可以直接使用,避免了重复获取Method对象的开销。
这里就不再举例说明了,感兴趣的同学可以自行查阅资料进行深入学习。
异常处理
有效的处理异常可以保证程序的稳定性和可靠性。但异常的处理对性能还是有一定的影响的,这一点常常被人忽视。影响性能的具体表现为:
响应延迟:当异常被抛出时,Java虚拟机需要查找并执行相应的异常处理程序,这会导致一定的延迟。如果程序中存在大量的异常处理,这些延迟可能会累积,导致程序的整体性能下降。
内存占用:异常处理需要在堆栈中创建异常对象,这些对象需要占用内存。如果程序中存在大量的异常处理,这些异常对象可能会占用大量的内存,导致程序的整体内存占用量增加。
CPU占用:异常处理需要执行额外的代码,这会导致CPU占用率增加。如果程序中存在大量的异常处理,这些额外的代码可能会导致CPU占用率过高,导致程序的整体性能下降。
一些基准测试显示,异常处理可能会导致程序的性能下降几个百分点。在Java虚拟机规范中提到,在没有异常发生的情况下,基于堆栈的方法调用可能比基于异常的方法调用快2-3倍。此外,一些实验表明,在异常处理程序中使用大量的try-catch语句,可能会导致性能下降10倍以上。
为避免这些问题,在编写代码时谨慎地使用异常处理机制,并确保对异常进行适当的记录和报告,这里建议不要使用异抛出异常的方式来处理业务逻辑,而是用条件判断。异常捕捉是用来处理不期望发生的事情,而错误码则用来处理可能会发生的事。举个例子:
public ServiceResponse < String > badCase4Throw ( String param1,String param2 ){
Assert. notNull ( param1, "param1 is null!" ) ;
Assert. notNull ( param1, "param2 is null!" ) ;
return new ServiceResponse ( ResponseCodeEnum. PARAM_ERROR ) ;
return new ServiceResponse <>() ;
/**
* 反面教材示例
* @param param1
* @param param2
* @return
*/
public ServiceResponse<String> badCase4Throw(String param1,String param2){
try {
Assert.notNull(param1, "param1 is null!");
Assert.notNull(param1, "param2 is null!");
// do something
}catch (Throwable e){
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
return new ServiceResponse<>();
}
/**
* 反面教材示例
* @param param1
* @param param2
* @return
*/
public ServiceResponse<String> badCase4Throw(String param1,String param2){
try {
Assert.notNull(param1, "param1 is null!");
Assert.notNull(param1, "param2 is null!");
// do something
}catch (Throwable e){
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
return new ServiceResponse<>();
}
public ServiceResponse < String > normCase4Throw ( String param1, String param2 ) {
if ( StringUtils. isEmpty ( param1 )) {
return new ServiceResponse ( ResponseCodeEnum. PARAM_ERROR ) ;
if ( StringUtils. isEmpty ( param2 )) {
return new ServiceResponse ( ResponseCodeEnum. PARAM_ERROR ) ;
return new ServiceResponse ( ResponseCodeEnum. SYSTEM_ERROR , e. getMessage ()) ;
return new ServiceResponse <>() ;
/**
* 正面教材示例
*
* @param param1
* @param param2
* @return
*/
public ServiceResponse<String> normCase4Throw(String param1, String param2) {
if (StringUtils.isEmpty(param1)) {
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
if (StringUtils.isEmpty(param2)) {
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
try {
// do something
} catch (Throwable e) {
return new ServiceResponse(ResponseCodeEnum.SYSTEM_ERROR, e.getMessage());
}
return new ServiceResponse<>();
}
/**
* 正面教材示例
*
* @param param1
* @param param2
* @return
*/
public ServiceResponse<String> normCase4Throw(String param1, String param2) {
if (StringUtils.isEmpty(param1)) {
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
if (StringUtils.isEmpty(param2)) {
return new ServiceResponse(ResponseCodeEnum.PARAM_ERROR);
}
try {
// do something
} catch (Throwable e) {
return new ServiceResponse(ResponseCodeEnum.SYSTEM_ERROR, e.getMessage());
}
return new ServiceResponse<>();
}
日志处理
LOGGER. info ( "result:" + JsonUtil. write2JsonStr ( contextAdContains ) + ", logid = " + DigitThreadLocal. getLogId ()) ;
LOGGER.info("result:" + JsonUtil.write2JsonStr(contextAdContains) + ", logid = " + DigitThreadLocal.getLogId());
LOGGER.info("result:" + JsonUtil.write2JsonStr(contextAdContains) + ", logid = " + DigitThreadLocal.getLogId());
以上示例代码中,类似的日志打印方式很常见,难道有什么问题吗?
性能问题:每次使用+进行字符串拼接时,都会创建一个新的字符串对象,这可能会导致内存分配和垃圾回收的开销增加;
可读性问题:使用+进行字符串拼接时,代码可能会变得难以阅读和理解,特别是在需要连接多个字符串时;
如果日志级别调整到ERROR模式,希望日志的字符串内容不需要进行加工计算,但这种写法,即使日志处于不需要打印的模式,日志内容也进行了无效计算。
特别实在请求量和日志打印量比较高的场景下,日志内容的序列化和写文件操作,对服务的耗时影响可以达到10%,甚至更多。
临时对象
临时对象通常是指在方法内部创建的对象。大量创建临时对象会导致Java虚拟机频繁进行垃圾回收,从而影响程序的性能。也会占用大量的内存空间,从而导致程序崩溃或者出现内存泄漏等问题。为了避免大量创建临时对象,在编码时,可以采取以下措施:
字符串拼接中,使用StringBuilder或StringBuffer进行字符串拼接,避免使用连接符,每次都创建新的字符串对象;
在集合操作中,尽量使用批量操作,如addAll、removeAll等,避免频繁的add、remove操作,触发数组的扩容或者缩容;
在正则表达式中,可以使用Pattern.compile()方法预编译正则表达式,避免每次都创建新的Matcher对象;
尽量使用基本数据类型,避免使用包装类,因为包装类的创建和销毁都会产生临时对象;
尽量使用对象池的方式创建和管理对象,比如使用静态工厂方法创建对象,避免使用new关键字创建对象,因为静态工厂方法可以重用对象,避免创建新的临时对象。
临时对象的生命周期应该尽可能短,以便及时释放内存资源。临时对象的生命周期过长通常是由以下原因引起的:
对象未被正确地释放:如果在方法执行完毕后,临时对象没有被正确地释放,就会导致内存泄漏风险;
对象过度共享:如果临时对象被过度共享,就可能会导致多个线程同时访问同一个对象,从而导致线程安全问题和性能问题;
对象创建过于频繁:如果在方法内部频繁地创建临时对象,就会导致内存开销过大,可能会引起性能甚至内存溢出问题。
及时释放对象:在方法执行完毕后,应该及时释放临时对象(比如主动将对象设置为null),以便回收内存资源;
避免过度共享:在多线程环境下,应该避免过度共享临时对象,可以使用局部变量或ThreadLocal等方式来避免共享问题;
对象池技术:使用对象池技术可以避免频繁创建临时对象,从而降低内存开销。对象池可以预先创建一定数量的对象,并在需要时从池中获取对象,使用完毕后再将对象放回池中。
锁的粒度
volatile是Java中的一个关键字,用于修饰变量。它的作用是保证被volatile修饰的变量在多线程环境下的可见性和禁止指令重排序。volatile可以看做是轻量级的sychronized,虽然volatile无法保证原子性,但是如果对某个共享变量是纯赋值操作和读取操作,而没有其他额外的操作,那么就可以使用volatile代替sychronized,因为赋值本身是原子的,而volatile又保证了可见性,最终也就保证了线程安全。
对象锁(也称为内置锁或监视器锁):它是Java中最基本的锁机制,每个Java对象都有一个内置锁。通过synchronized关键字可以获取对象锁。对象锁的作用是保护对象的状态,同一时刻只有一个线程可以获取该对象的锁,其他线程需要等待该线程释放锁后才能获取锁。对象锁的粒度最小,适用于对共享资源的并发访问。
类锁:类锁是针对类的,它是在类加载时创建的。通过synchronized关键字加在静态方法上可以获取类锁。类锁的作用是保护静态变量和静态方法,在同一时刻只有一个线程可以获取该类的锁,其他线程需要等待该线程释放锁后才能获取锁。类锁的粒度比对象锁大,适用于对静态资源的并发访问。
读写锁:读写锁分为读锁和写锁,读锁可以被多个线程同时获取,但写锁只能被一个线程获取。在读多写少的场景下,使用读写锁可以提高并发性能。
分段锁:分段锁是一种细粒度锁,它将一个大的数据结构分成多个小的数据结构,每个小的数据结构都有自己的锁。通过这种方式可以降低锁的竞争,提高并发性能。
自旋锁:自旋锁是一种基于忙等待的锁,它不会使线程进入阻塞状态,而是在获取锁失败时不断重试。自旋锁的优点是减少线程上下文切换的开销,缺点是会浪费CPU资源。自旋锁适用于临界区很小的情况。
信号量(Semaphore):信号量是一种计数器,它可以控制对共享资源的访问。在获取信号量时,如果计数器大于0,则可以继续访问共享资源,否则需要等待其他线程释放资源后再获取。信号量的粒度比前面几种锁都大,适用于对多个资源的并发访问。
锁的粒度越小,并行 度和效率也就越高,在进行锁的使用的时候需要格外关注。
外延扩展:为什么说DCL单例模式中,一定要给单例对象属性添加volatile关键字?这是因为volatile可以实现可见性和禁止指令重排,这样可以保证其他线程不会拿到一个半初始化对象,避免带来线程安全问题。
private volatile static Singleton INSTANCE;
public static Singleton getInstance () {
synchronized ( Singleton. class ) {
//注意:在栈引用与堆对象进行绑定和对象初始化的过程的顺序就有可能出现指令重排,
//导致其它线程拿到了未初始化成员变量的对象引用,比如取到的number=0
INSTANCE = new Singleton () ;
public class Singleton {
private int number;
//一定要加上volatile关键字
private volatile static Singleton INSTANCE;
//私有构造器
private Singleton(){
this.number = 10;
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
//注意:在栈引用与堆对象进行绑定和对象初始化的过程的顺序就有可能出现指令重排,
//导致其它线程拿到了未初始化成员变量的对象引用,比如取到的number=0
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
public class Singleton {
private int number;
//一定要加上volatile关键字
private volatile static Singleton INSTANCE;
//私有构造器
private Singleton(){
this.number = 10;
}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
//注意:在栈引用与堆对象进行绑定和对象初始化的过程的顺序就有可能出现指令重排,
//导致其它线程拿到了未初始化成员变量的对象引用,比如取到的number=0
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
小结
正所谓:“不积跬步,无以至千里;不积小流,无以成江海”。以上列举的编码细节,都会直接或间接的影响服务的执行效率,只是影响多少的问题。现实中,有时候我们不必过于苛求,但它们有一个共同的注脚:极客精神。
设计优化
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
合理使用缓存可以有效提高应用程序的性能,缩短数据访问时间,降低对数据源的依赖性。缓存可以进行多层级的设计,举例,为了提高运行效率,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>
public class LRUHashMap < K, V > extends LinkedHashMap < K, V > {
private final int maxSize;
* @param maxSize the max size
public LRUHashMap ( int maxSize ) {
super ( maxSize, 0.75 f, true ) ;
* @param accessOrder true表示按访问顺序排序,false为插入顺序
public LRUHashMap ( int maxSize, boolean accessOrder ) {
//0.75是默认值,true表示按访问顺序排序,false为插入顺序
super ( maxSize, 0.75 f, accessOrder ) ;
protected boolean removeEldestEntry ( Map. Entry < K, V > eldest ) {
return super. size () > maxSize;
/**
* 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;
}
}
/**
* 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;
}
}
异步可以提高程序的性能和响应能力,使其能更高效地处理大规模数据或并发请求。其底层原理涉及到操作系统的多线程、事件循环、任务队列以及回调函数等关键技术,除此之外,异步的思想在应用架构设计方面也有广泛的应用。常规的多线程,消息队列,响应式编程等异步处理方案这里就不再展开介绍了,这里介绍两个大家可能容易忽视但实用技能:非阻塞IO和 协程。
非阻塞IO
Java Servlet 3.0规范中引入了异步Servlet的概念,可以帮助开发者提高应用程序的性能和并发处理能力,其原理是非阻塞IO使用单线程同时处理多个请求,避免了线程切换和阻塞的开销,特别是在读取大文件或者进行复杂耗时计算场景时,可以避免阻塞其他请求的处理。Spring MVC框架中也提供了相应的异步处理方案。
@ GetMapping ( "/async/callable" )
public WebAsyncTask < String > asyncCallable () {
Callable < String > callable = () - > {
return new WebAsyncTask <>( 10000 , callable ) ;
@GetMapping("/async/callable")
public WebAsyncTask<String> asyncCallable() {
Callable<String> callable = () -> {
// 执行异步操作
return "异步任务已完成";
};
return new WebAsyncTask<>(10000, callable);
}
@GetMapping("/async/callable")
public WebAsyncTask<String> asyncCallable() {
Callable<String> callable = () -> {
// 执行异步操作
return "异步任务已完成";
};
return new WebAsyncTask<>(10000, callable);
}
@ GetMapping ( "/async/deferredresult" )
public DeferredResult < String > asyncDeferredResult () {
DeferredResult < String > deferredResult = new DeferredResult <>( 10000L ) ;
deferredResult. setResult ( "DeferredResult异步任务已完成" ) ;
@GetMapping("/async/deferredresult")
public DeferredResult<String> asyncDeferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>(10000L);
// 异步处理完成后设置结果
deferredResult.setResult("DeferredResult异步任务已完成");
return 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 ()
Thread thread = Thread.ofVirtual()
.name("Virtual Threads")
.unstarted(runnable);
Thread thread = Thread.ofVirtual()
.name("Virtual Threads")
.unstarted(runnable);
也可以通过一个线程工厂类进行创建:
ThreadFactory factory = Thread. ofVirtual () . factory () ;
ThreadFactory factory = Thread.ofVirtual().factory();
ThreadFactory factory = Thread.ofVirtual().factory();
虚拟线程运行的载体必须是线程,同一个线程中可以运行多个虚拟线程实例。
并行处理的思想在大数据、多任务、流水线处理、模型训练等各个方面发挥着重要作用,包括前面介绍的异步(多线程,协程,消息等),也是建立在并行的基础上。在应用层面,典型的场景有:
分布式计算框架中的MapReduce就是采用一种分而治之的思想设计出来的,将复杂或计算量大的任务,切分成一个个小的任务,小任务分别在不同的线程或服务器上并行的执行,最终再汇总每个小任务的结果。
边缘计算(Edge Computing)是一种分布式计算范式,它将计算、存储和网络服务的部分功能从云数据中心延伸至离数据源更近的地方,即网络的边缘。这种计算方式能够实现低延迟、节省带宽、提高数据安全性以及实时处理与分析等优势。
在代码实现方面,做好解耦设计,接下来就可以进行并行设计了,比如:
池化就是初始预设资源,降低每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。典型的场景就是线程池,数据库连接池,业务处理结果缓存池等。
以数据库连接池为例,其本质是一个 socket 的连接。为每个请求打开和维护数据库连接,尤其是动态数据库驱动的应用程序的请求,既昂贵又浪费资源。为什么这么说呢?以MySQL数据库建立连接(TCP协议)为例,建立连接总共分三步:
建立TCP连接,通过三次握手实现;
服务器发送给客户端「握手信息」 ,客户端响应该握手消息;
客户端「发送认证包」 ,用于用户验证,验证成功后,服务器返回OK响应,之后开始执行命令。
简单粗略统计,完成一次数据库连接,客户端和服务器之间需要至少往返7次,总计平均耗时大约在200ms左右,这对于很对C端服务来说,几乎是不能接受的。
落实到代码编写层面,也可以借助这一思想来优化程序执行性能。
公用的数据可以全局只定义一份,比如使用枚举,static修饰的容器对象等;
根据实际情况,提前设置List,Map等容器对象的初始化容量大小,防止后面的扩容,对性能的影响;
一般需要池化的内容,都是需要预处理的,比如为了保证服务的稳定性,线程池和数据库连接池等需要池化的内容在JVM容器启动时,处理真正请求之前,对这些池化内容进行预处理,等到真正的业务处理请求过来时,可以正常的快速处理。除此之外,预处理还可以体现在系统架构层面。
为了提高响应性能,将部分业务数据提前预加载到内存中;
为了减轻CPU压力,将计算逻辑提前执行,直接将计算后的结果数据保存下来,直接供调用方使用;
为了降低网络带宽成本,将传输数据通过压缩算法进行压缩处理,到了目标服务,在进行解压,获得原始数据;
Myibatis为了提高SQL语句的安全性和执行效率,也引入了预处理的概念。
总结
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目
性能优化是程序开发过程中绕不过去一个课题,本文聚焦代码和设计两个方面,从CPU硬件到JVM容器,从缓存设计到数据预处理,全面的展现了性能优化的实施方向和落地细节。阐述的过程没有追求各个方向的面面俱到,但都给到了一些场景化案例,来辅助理解和思考,起到抛砖引玉的效果。最后,希望本文能够为你带来思考和帮助。
打造SAAS化服务的会员徽章体系,可以作为标准的产品化方案统一对外输出。结合现有平台的通用能力,实现会员行为全路径覆盖,并能结合企业自身业务特点,规划相应的会员精准营销活动,提升会员忠诚度和业务的持续增长。
底层能力:维护用户基础数据、行为数据建模、用户画像分析、精准营销策略的制定
▪功能支撑:会员成长体系、等级计算策略、权益体系、营销底层能力支持
▪用户活跃:会员关怀、用户触达、活跃活动、业务线交叉获客、拉新促活
本文仅供学习!所有权归属原作者。侵删!文章来源: 京东 技术 -刘慧卿 :http://mp.weixin.qq.com/s/LcUksJvmBN2oczI1k7J6bA
文章评论