架构师日记-从代码到设计的性能优化指南 | 京东云技术团队

一 前言

服务性能是指服务在特定条件下的响应速度、吞吐量和资源利用率等方面的表现。据统计,性能优化方面的精力投入,通常占软件开发周期的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硬件数据:

如何简单来区分访存密集型和计算密集型程序?

  1. 如果 IPC < 1.0, 很可能是 Memory stall 占主导,多半意味着访存密集型。

  2. 如果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,在运行时生成字节码,从而避免使用反射。

为什么动态字节码生成方式相比反射也可以提高执行效率呢?

  1. 动态字节码生成的方式在编译期就已经将类型信息确定下来,无需进行类型检查和转换;
  2. 动态字节码生成的方式可以直接调用方法,无需查找,提高了执行效率;
  3. 动态字节码生成的方式只需要在生成字节码时获取一次Method对象,多次调用时可以直接使用,避免了重复获取Method对象的开销;

这里就不再举例说明了,感兴趣的同学可以自行查阅资料进行深入学习。

异常处理

有效的处理异常可以保证程序的稳定性和可靠性。但异常的处理对性能还是有一定的影响的,这一点常常被人忽视。影响性能的具体表现为:

  • 响应延迟:当异常被抛出时,Java虚拟机需要查找并执行相应的异常处理程序,这会导致一定的延迟。如果程序中存在大量的异常处理,这些延迟可能会累积,导致程序的整体性能下降。
  • 内存占用:异常处理需要在堆栈中创建异常对象,这些对象需要占用内存。如果程序中存在大量的异常处理,这些异常对象可能会占用大量的内存,导致程序的整体内存占用量增加。
  • CPU占用:异常处理需要执行额外的代码,这会导致CPU占用率增加。如果程序中存在大量的异常处理,这些额外的代码可能会导致CPU占用率过高,导致程序的整体性能下降。

一些基准测试显示,异常处理可能会导致程序的性能下降几个百分点。在Java虚拟机规范中提到,在没有异常发生的情况下,基于堆栈的方法调用可能比基于异常的方法调用快2-3倍。此外,一些实验表明,在异常处理程序中使用大量的try-catch语句,可能会导致性能下降10倍以上。

为避免这些问题,在编写代码时谨慎地使用异常处理机制,并确保对异常进行适当的记录和报告,避免过度使用异常处理机制。

日志处理

先看以下代码:

LOGGER.info("result:" + JsonUtil.write2JsonStr(contextAdContains) + ", logid = " + DigitThreadLocal.getLogId());

以上示例代码中,类似的日志打印方式很常见,难道有什么问题吗?

  • 性能问题:每次使用+进行字符串拼接时,都会创建一个新的字符串对象,这可能会导致内存分配和垃圾回收的开销增加;
  • 可读性问题:使用+进行字符串拼接时,代码可能会变得难以阅读和理解,特别是在需要连接多个字符串时;
  • 如果日志级别调整到ERROR模式,我们希望日志的字符串内容不需要进行加工计算,但这种写法,即使日志处于不需要打印的模式,日志内容也进行了无效计算;

特别实在请求量和日志打印量比较高的场景下,日志内容的序列化和写文件操作,对服务的耗时影响可以达到10%,甚至更多。

临时对象

临时对象通常是指在方法内部创建的对象。大量创建临时对象会导致Java虚拟机频繁进行垃圾回收,从而影响程序的性能。也会占用大量的内存空间,从而导致程序崩溃或者出现内存泄漏等问题。

为了避免大量创建临时对象,在编码时,可以采取以下措施:

  1. 字符串拼接中,使用StringBuilder或StringBuffer进行字符串拼接,避免使用连接符,每次都创建新的字符串对象;
  2. 在集合操作中,尽量使用批量操作,如addAll、removeAll等,避免频繁的add、remove操作,触发数组的扩容或者缩容;
  3. 在正则表达式中,可以使用Pattern.compile()方法预编译正则表达式,避免每次都创建新的Matcher对象;
  4. 尽量使用基本数据类型,避免使用包装类,因为包装类的创建和销毁都会产生临时对象;
  5. 尽量使用对象池的方式创建和管理对象,比如使用静态工厂方法创建对象,避免使用new关键字创建对象,因为静态工厂方法可以重用对象,避免创建新的临时对象;

临时对象的生命周期应该尽可能短,以便及时释放内存资源。临时对象的生命周期过长通常是由以下原因引起的:

  1. 对象未被正确地释放:如果在方法执行完毕后,临时对象没有被正确地释放,就会导致内存泄漏风险;
  2. 对象过度共享:如果临时对象被过度共享,就可能会导致多个线程同时访问同一个对象,从而导致线程安全问题和性能问题;
  3. 对象创建过于频繁:如果在方法内部频繁地创建临时对象,就会导致内存开销过大,可能会引起性能甚至内存溢出问题;

为避免临时对象的生命周期过长,建议采取以下措施:

  1. 及时释放对象:在方法执行完毕后,应该及时释放临时对象(比如主动将对象设置为null),以便回收内存资源;
  2. 避免过度共享:在多线程环境下,应该避免过度共享临时对象,可以使用局部变量或ThreadLocal等方式来避免共享问题;
  3. 对象池技术:使用对象池技术可以避免频繁创建临时对象,从而降低内存开销。对象池可以预先创建一定数量的对象,并在需要时从池中获取对象,使用完毕后再将对象放回池中;

小结

正所谓:“不积跬步,无以至千里;不积小流,无以成江海”。以上列举的编码细节,都会直接或间接的影响服务的执行效率,只是影响多少的问题。现实中,有时候我们不必过于苛求,但它们有一个共同的注脚:极客精神。

三 设计优化

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端服务来说,几乎是不能接受的。

落实到代码编写层面,也可以借助这一思想来优化我们的程序执行性能。

  1. 公用的数据可以全局只定义一份,比如使用枚举,static修饰的容器对象等;
  2. 根据实际情况,提前设置List,Map等容器对象的初始化容量大小,防止后面的扩容,对性能的影响;
  3. 亨元设计模式的应用等;

3.5 预处理

一般需要池化的内容,都是需要预处理的,比如为了保证服务的稳定性,线程池和数据库连接池等需要池化的内容在JVM容器启动时,处理真正请求之前,对这些池化内容进行预处理,等到真正的业务处理请求过来时,可以正常的快速处理。除此之外,预处理还可以体现在系统架构层面。

  1. 为了提高响应性能,将部分业务数据提前预加载到内存中;
  2. 为了减轻CPU压力,将计算逻辑提前执行,直接将计算后的结果数据保存下来,直接供调用方使用;
  3. 为了降低网络带宽成本,将传输数据通过压缩算法进行压缩处理,到了目标服务,在进行解压,获得原始数据;
  4. Myibatis为了提高SQL语句的安全性和执行效率,也引入了预处理的概念;

四 总结

性能优化是程序开发过程中绕不过去一个课题,本文聚焦代码和设计两个方面,从CPU硬件到JVM容器,从缓存设计到数据预处理,全面的展现了性能优化的实施方向和落地细节。阐述的过程没有追求各个方向的面面俱到,但都给到了一些场景化案例,来辅助理解和思考,起到抛砖引玉的效果。

作者:京东零售 刘慧卿

内容来源:京东云开发者社区

本文转载于网络 如有侵权请联系删除

相关文章

  • ceph分布式存储学习指南 实战

    1、安装完虚拟机后,更改名字,设置/etc/hosts文件 2、ceph-deploy工具部署image.png3、ceph-deploy要和其他服务器SSH登录 4、所有节点添加EPEL仓库 5、yuminstall-yceph-deployimage.png6、将ceph安装到所有节点:emperor为版本号ceph-deployinstall--releaseemperorceph-node1ceph-node2ceph-node37、检查:ceph-v/cephstatus8、创建monitor:ceph-deploymoncreate-initial 9、ceph-node1创建OSDimage.pngimage.png10、ceph集群最少需要一个monitor处于运行状态。要提高高可用性,需要大于1的奇数个monitor,3个或5个。它使用paxos算法维护多数仲裁。11、关闭防火墙或开通某些端口12、ceph-deploymoncreateceph-node2ceph-deploymoncreateceph-node3检查状态:cephstatus13、所有节点配置NT

  • 高速串行总线设计基础(一)同步时序模型介绍

    前言芯片间通信的时序模型系统同步源同步自同步参考文章前言高速信号设计涉及到方方面面的知识积累,也许你认为即使没有掌握甚至没有听过一些高速设计的专业术语,也没有关系?因为专业集成的IP可以帮你解决这一问题,但殊不知,根基不牢,地动山摇,基础不牢固,你永远成不了一个expert,甚至成不了一个出色的工程师。本系列文章通过联系高速串行的相关知识,从基础到进阶,可以助你很好地理解高速串行设计中的重难点。短期可助于理解各厂家集成IP,高速协议等,长期有助于技术积累,提高技术修养。相信无论新手还是行家,都会有所收益。以下是本系列第一篇,同步时序模型介绍。芯片间通信的时序模型大体上,有三种时序模型用于芯片间通信,那就是系统同步,源同步以及自同步,下面分别介绍:系统同步所谓的系统同步,即在IC之间通信中,外部公共时钟应用于两个IC之间,并用于数据发送以及接收。系统同步是最常用的同步方式,其示意图如下:系统同步电路图这是一种很常见的电路模型,在IC应届生招聘时,这种类型的时序分析题,也最为常见。如下图为上图的详细时序模型:系统同步时序模型阴影部分为必须要考虑的延迟。由于具体时序分析计算非本文重点,因此不

  • 六项任务、多种数据类型,谷歌、DeepMind提出高效Transformer评估基准

    机器之心报道编辑:魔王自诞生以来,Transformer在不同领域得到了广泛应用,研究人员也提出了许多高效Transformer模型。如何评估这类模型呢?最近,谷歌和DeepMind提出了一项系统化的统一基准——Long-RangeArena,重点关注长语境场景下的模型质量评估。基准项目地址:https://github.com/google-research/long-range-arena论文地址:https://arxiv.org/pdf/2011.04006.pdfTransformer在多个模态(语言、图像、蛋白质序列)中获得了SOTA结果,但它存在一个缺点:自注意力机制的平方级复杂度限制了其在长序列领域中的应用。目前,研究人员提出大量高效Transformer模型(「xformer」),试图解决该问题。其中很多展示出了媲美原版Transformer的性能,同时还能有效降低自注意力机制的内存复杂度。谷歌和DeepMind的研究人员对比了这些论文的评估和实验设置,得到了以下几点发现:首先,高效Transformer缺少统一的基准测试,使用的任务类型也多种多样:每个模型在不同的任

  • Android Studio3.6.3 当前最新版本数据库查找与导出方法(图文详解)

    一、SQLite安装包准备  本文章主要是针对安装了AndroidStudio3.6.3版本(AndroidStudio以下简称为AS)所做的SQLite教程,  博主这边安装的是,  由于SQL语言基本大同小异,仅仅是管理数据库的软件不大相同,所以说数据库使用方法类似,具体安装流程参考网络上其他文章,本文暂不提供安装流程,安装完毕即可。二、重中之重—-AndroidStudio3.0版本起弃用了之前使用的ADM  博主也是刚发现自AS3.0开始弃用AndroidDeviceMonitor(即ADM)这个没多久,  查数据库需要经过AS内置的,以下简称为DFE。  而这个DFE在AS软件右下角很容易看见。后续点开查询数据库方法在下面列出,  图可能会比较多,写的尽量详细一点。三、DFE类似之前版本的ADM,都需要启动手机模拟器(即AVD)才会出现路径  运行app并点开DFE,则可如图所示有很多文件  而我们需要查询的数据库路径就在data\data\com.example.chuxuesqlite    (加粗放大路径是安装包的存放路径包名,你们的和博主的不同哦!) 博主这边已经使用

  • LeetCode题组:第13题-罗马数字转整数

    1.题目:回文数罗马数字包含以下七种字符:I,V,X,L,C,D和M。字符数值I1V5X10L50C100D500M1000例如,罗马数字2写做II,即为两个并列的1。12写做XII,即为X+II。27写做XXVII,即为XX+V+II。通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如4不写做IIII,而是IV。数字1在数字5的左边,所表示的数等于大数5减小数1得到的数值4。同样地,数字9表示为IX。这个特殊的规则只适用于以下六种情况:I可以放在V(5)和X(10)的左边,来表示4和9。X可以放在L(50)和C(100)的左边,来表示40和90。C可以放在D(500)和M(1000)的左边,来表示400和900。给定一个罗马数字,将其转换成整数。输入确保在1到3999的范围内。示例1:输入:“III” 输出:3 示例2:输入:“IV” 输出:4 示例3:输入:“IX” 输出:9 示例4:输入:“LVIII” 输出:58 解释:L=50,V=5,III=3. 示例5:输入:“MCMXCIV” 输出:1994 解释:M=1000,CM=900,XC=90,IV=4. 2.

  • 使用python 爬梨视频

    刚开始学习python勿喷 第一步去官网下载python3^版本 下载链接https://www.python.org/downlo...如果是window系统需要添加一下环境变量下面是代码pyhton爬虫梨视频需要先下载request模块 pipinstallrequestsimportrequests importre importos importtime fromurllib.requestimporturlretrieve#下载模块 defvideo_DL(url): header={"User-Agent":"Mozilla/5.0(WindowsNT10.0;Win64;x64;rv:63.0)Gecko/20100101Firefox/63.0"} #url="https://www.pearvideo.com/category_6" response=requests.get(url,headers=header) html=response.text reg='<ahref="(.

  • Angular学习(01)-架构概览

    声明本系列文章内容梳理自以下来源:Angular官方中文版教程官方的教程,其实已经很详细且易懂,这里再次梳理的目的在于复习和巩固相关知识点,刚开始接触学习Angular的还是建议以官网为主。因为这系列文章,更多的会带有我个人的一些理解和解读,由于目前我也才刚开始接触Angular不久,在该阶段的一些理解并不一定是正确的,担心会有所误导,所以还是以官网为主。正文-架构概览接触Angular大概一个月吧,期间写了个项目,趁现在稍微有点时间,来回顾梳理一下。其实,如果前端网站并不是特别复杂,那么使用Angular无非也就是常跟几个重要的知识点打交道,在官网的核心知识的第一节中就将这些知识点罗列出来了,也就是:架构概览。Angular架构概览.png画了这个图来大概表示下Angular的架构概览,基本涉及到一些常见的重要的知识点了,比如:模块路由组件模板服务指令管道不同的类型,文件名通常会都按照一定的规范来命名,以便直接看出该文件的角色。当然,文件命名只是给开发人员来方便维护、辨别,对于Angular来说,这些都是一份份的ts文件代码,所以,都需要在相对应的文件中加上一些装饰器比如:@Dire

  • 推荐一个CodeProject上的SlideForm控件

    CodeProject有一篇文章介绍了怎么实现一个SlideForm,非常不错,收藏在此.http://www.codeproject.com/KB/dialog/csslideform.aspx使用方法很简单,引用SlideForm.dll,然后添加一个Form修改它从SlideDialog.SlideDialog继承publicclassMySlideForm:SlideDialog.SlideDialog并重新实现构造函数,调用父类的构造函数,传入参数:public MySlideForm(Form poOwner, float pfStep) : base(poOwner, pfStep) {   InitializeComponent(); }复制 这样就实现了一个SlideForm,接着就是使用这个SlideForm了MySlideForm _oSlideForm;  _oSlideForm = new MySlideForm(this, 0.1f);  _oSlideForm.SlideDirection = SlideDialog.SlideDialog.SLIDE_D

  • Java中的异常处理1使用try,catch异常继承架构该抓还是该抛

    我们通过一个简单的实例程序来了解一下什么是java中的异常处理使用try,catch看下面这个程序:packageExceptionNote; importjava.util.Scanner; publicclassAverage{ publicstaticvoidmain(String[]args){ Scannerconsole=newScanner(System.in); doublesum=0; intcount=0; while(true){ intnumber=console.nextInt(); if(number==0) break; sum+=number; count++; } System.out.printf("Average%.2f%n",sum/count); } }复制这个程序可以让用户输入任意个整数,以0作为结束的标志,最后会显示输入整数的平均值。 下面我们进行简单的测试如果用户正确的输入每个整数,那么自然,程序会顺利显示结果Paste_Image.png但如果用户输入错误呢,就会出现如下错误信息Paste_Image.pn

  • 构建混合网络要素:SDN+网络虚拟化

    编者按:作者首先简单介绍何为SDN、网络虚拟化,随后阐明当前形势,在这样的形势下构建混合网络部署SDN和虚拟化时需要考虑哪些问题呢?且听作者一一道来。部署软件定义网络(SDN)和网络虚拟化技术能够给IT企业带来很多优势,包括快速配置网络资源、降低运营成本以及改善网络可视性、政策和业务流程。而IT管理人员面临的挑战是,面对各种各样的SDN技术,如何做出正确选择以帮助企业在短期内获得可衡量的优势,并确保其选择的技术可以发展为下一代网络架构。在本文中,我们将探讨IT管理人员在部署SDN和网络虚拟化时需主要考虑的因素。定义SDN和网络虚拟化SDN被定义为具有以下功能: •分离“控制和数据平面”:交换机和路由器的智能可从数据包转发引擎中分离出来。 •可编程性:能够集中更改流量、分隔网络以及提供应用级QoS提高网络灵活性SDN技术可以用在数据中心、WAN,并可作为电信(例如光纤网络)传输网络的一部分。虚拟网络(也称为虚拟覆盖网络)使用隧道在传统(以太网)物理网络之上创建虚拟网络。网络虚拟化利用VXLAN或NVGRE等协议来提供3层网络隧道,并允许虚拟机(VM)在数据中心内的网络间或在数据中心之间迁

  • iOS开发-代理模式

    代理模式有的时候也被称之为委托模式,但是实际上两者是有分别的,代理模式为另一个对象提供一个替身或占位符访问这个对象,代理对象和控制访问对象属于同一类,委托对象和对象不一定属于同一类。两者都可以控制类的访问,访问代理的方法A也就意味着访问对象的方法A,访问委托对象方法A执行的是可以是对象的方法B。从实际开发的角度看,委托属于代理模式的扩大版,并没有那么多的限制。 基础知识 代理模式相对比较简单,可以简单的看一下UML类图:   代理模式以便管理客户对对象的访问,管理访问的方式有很多种。远程代理管理客户和远程对象之间的交互,虚拟代理控制方式实例开销大的对象,代理模式和装饰者模式有点相似,装饰者模式为对象加上行为,代理模式控制对另一个对象的访问。 实战研究 我们就根据上面的UML的类图写一个比较常规的例子,先定义定义Subject: @protocolSubjectProtocol<NSObject> -(void)displayImage; @end @interfaceSubject:NSObject<SubjectProtocol> @en

  • 定义在方法中的内部类能否访问该方法的局部变量?

    定义在方法中的内部类当然是可以访问方法中的局部变量的,访问的方法就是在局部变量上添加final关键字 (1)内部类是外部类的一个成员,就像外部类的成员方法一样,所以内部类有权限访问外部类的所有成员,包括private的。(2)内部类不能访问外部类方法中的局部变量,除非变量是final的(一般发生在方法中定义的内部类)。这是因为局部变量的生命周期原因。 classOuter{   privateinta;   publicclassInner{       privateinta;       publicvoidmethod(inta){           a++;   //局部变量     &nbs

  • 踹树(Trie 字典树)

    Trie字典树 ~~比KMP简单多了,无脑子选手学不会KMP,不会结论题~~ 自己懒得造图了OIWIKI真棒 字典树大概长这么个亚子 呕吼真棒 就是将读进去的字符串根据当前的字符是什么和所处的位置构成一棵树 例如图中\(1-->2-->5\)这一条路就是字符串\(aa\)那\(1-->4-->8-->13\)就是字符串\(cab\) 貌似也没有什么东西的,题目的话基本就是套板子,唯一奇怪的就是把一个数二级化建树跑一些奇奇怪怪的东西 字符串: structTrie{ inttrie[][],Trie_num; voidadd(char*s){ intnow=0, for(inti=1;i<=len;i++){ if(!trie[now][s[i]])trie[now][s[i]]=+++Trie_num; now=trie[now][s[i]]; } } } 复制 二进制: structTrie{ inttire[][],Tire_num; voidadd(intx){ intnow=0, for(inti=31;i>=0;i--){ in

  • 【UWP】使用Action代替Command

    在Xaml中,说到绑定,我们用的最多的应该就是ICommand了,通过Command实现ViewModel到View之间的命令处理,例如Button默认就提供了Command支持,如下 Xaml: <ButtonContent="TestWithCommand"Command="{BindingTestCommand}"/>复制 ViewModel ///<summary>Providesabaseimplementationofthe<seecref="ICommand"/>interface.</summary> publicabstractclassCommandBase:ICommand { ///<summary>Getsavalueindicatingwhetherthecommandcanexecuteinitscurrentstate.</summary> publicabstractboolCanExecute{get;} ///<summary>Definesthemeth

  • js继承的几种方式

    最近在面试的时候,遇到过两次问继承实现的几种方式,这里能我给大家列举了以下的这几种,给大家参考参考 方式一:借助构造函数实现继承 这里使用的原理就是在Child里面,把Parent的this指向改为是Child的this指向,从而实现继承 functionParent(name){ this.name=name; } Parent.prototype.saiHi=function(){ console.log("hello") } functionChild(name,age,gender){ Parent.call(this,name) this.age=age; this.gender=gender; } letchild=newChild("王磊",20,"男") console.log(child.name);//王磊 child.sayHi();//UncaughtTypeError:child.sayHiisnotafunction复制 缺点:只能解决属性的继承,使用属性的值不重复,但是父级类别的方法不能继承 方式二:借助原型链实现继承 第二种方式就是把Child的原型

  • vue Eslint常用规范:

    "no-alert":0,//禁止使用alertconfirmprompt "no-array-constructor":2,//禁止使用数组构造器 "no-bitwise":0,//禁止使用按位运算符 "no-caller":1,//禁止使用arguments.caller或arguments.callee "no-catch-shadow":2,//禁止catch子句参数与外部作用域变量同名 "no-class-assign":2,//禁止给类赋值 "no-cond-assign":2,//禁止在条件表达式中使用赋值语句 "no-console":2,//禁止使用console "no-const-assign":2,//禁止修改const声明的变量 "no-constant-condition":2,//禁止在条件中使用常量表达式if(true)if(1) "no-continue":0,//禁止使用continue "no-control-regex":2,//禁止在正则表达式中使用控制字符 "no-debugger":2,//禁止使用debugger "no-delete-v

  • C语言I作业08

    问题 回答 这个作业属于哪个课程 C语言程序设计I 这个作业要求在哪里 [作业要求链接http://edu.cnblogs.com/campus/zswxy/CST2019-4/homework/9979 我在这个课程的目标是 了解函数编程,掌握相关语句的使用;熟练使用Markdown语法 这个作业在哪个具体方面帮助我实现目标 PTA分支题的作业和语言的使用;Markdown语法的使用 参考文献 《C语言程序设计》;参考链接; 1.PTA实验作业 1.1jmu-c-二进制转10进制 1.1.1数据处理 数据表达:该题目我用到了整型变量是s、flag,以及字符型变量number,且number为输入的字符型1或0,用来储存二进制。 数据处理:定义变量s来放转化后的值,flag来判断是不是二进制的数1或0,number用来放每个字符。 1.1.2实验代码截图 1.1.3造测试数据 输入数据 输出数据 说明 11111111 255 11111111通过二进制到十进制的转换计算为255 34 errorinput! 34不是二进制,

  • topcoder srm 580 div1 [FINISHED] (贪心, 区间统计, dp)

    problem1link 最优选择一定是在$2n$个端点中选出两个。 problem2link 分开考虑每个区间。设所有区间的左端点的最大值为$lc$,所有区间的右端点的最小值为$rc$.对于某个区间$L$,其实就是找最少的区间(包括$L$)能够完全覆盖区间$[lc,rc]$. 设$L$的左右端点为$L_{1},L_{2}$。考虑从$L_{1}$向左扩展,每次扩展一定是找一个区间$p$满足$p_{2}\geqL_{1}$且使得$p_{1}$最小。右端点向右扩展类似。 problem3link 对于后手来说,它一定是每次只放置一个障碍格子在先手目前的位置。否则,如果放置了太多,那么先手就容易决策出向左还是向右比较好。 对于先手来说,假设目前先手在$(r,c)$,那么如果$(r,c)$和$(r+1,c)$之间没有障碍,且轮到先手,应该直接跳到$(r+1,c)$,否则,那么由后手的策略来看,此时已经有的障碍一定是一个包含$(r,c)$的区间,此时先手可以尝试向左或者向右运动到障碍区间外一个格子。 这样就可以用$(r,c,left,right,tag)$来表示一个状态来进行动态规划。$(r,c

  • React中如何设置代理跨域请求数据

    方案一: //配置:package.json "proxy":"https://xxxx.com"复制   问题:只能代理一个服务器 方案二: 利用客户端代理中间件(http-proxy-middleware)完成,官网给了新的使用方式,在src下新建文件setupProxy.js加下面代码,无需单独应用,webpack会自动引入文件。 npminstallhttp-proxy-middleware-S复制   文件内容//verion<1.0版本 constproxy=require('http-proxy-middleware');//需要安装中间件 module.exports=function(app){ app.use( proxy("/api",{ target:'https://xxx.com', changeOrigin:true }) ); app.use( proxy("/v2",{ target:"https://xxx2.com", changeOrigin:true }) ); }; //组件:/api/xx...

  • 用命令创建MySQL数据库

    一、连接MYSQL   格式:mysql-h主机地址-u用户名-p用户密码   1、连接到本机上的MYSQL。   首先打开DOS窗口,然后进入目录mysql\bin,再键入命令mysql-uroot-p,回车后提示你输密码.注意用户名前可以有空格也可以没有空格,但是密码前必须没有空格,否则让你重新输入密码.   如果刚安装好MYSQL,超级用户root是没有密码的,故直接回车即可进入到MYSQL中了,MYSQL的提示符是:mysql>      2、连接到远程主机上的MYSQL。假设远程主机的IP为:110.110.110.110,用户名为root,密码为abcd123。则键入以下命令:   mysql-h110.110.110.110-uroot-p123;(注:u与root之间可以不用加空格,其它也一样)   3、退出MYSQL命令:exit(回车)   二、修改密码   格式:mysqladmin-u用户名-p旧密码password新密码。例如   1、给root加个密码ab12。首先在DOS下进入目录mysql\bin,然后键入以下命令   mysqladmin-uro

  • 2022-9-20

    cmd命令积累: win+r键  输入dxdiag命令,查看电脑本机的配置       宜将剩勇追穷寇,不可沽名学霸王。

相关推荐

推荐阅读