池化技术

池化技术

什么是池化技术

将对象放入池子,使用时从池中取,用完之后交给池子管理。通过优化资源分配的效率,达到性能的调优。

java中常见的池化技术有对象池,复用对象;连接池,复用连接;线程池,复用线程。

对象池

java中,万物皆对象,如果硬是要说连接池、线程池都是对象池看似也没问题。不过根据实际用途,把不用作连接和线程的对象,统一纳入到对象池这里。

对象池可以复用池中对象,避免对象频繁的创建销毁。一方面减少了创建销毁时资源的开销,同时也减少了垃圾回收器执行回收的次数,毕竟垃圾回收时会出现STW

常见的对象池框架:commons-pool2、object-pool、keyedObjectPool

连接池

当服务器与外部服务器需要交互时,就需要创建连接。常见的会有mysql连接、redis连接、netty连接、http连接。

以数据库连接为例,如果每一次连接都需要重新创建连接,执行完命令后再销毁,等下次执行再创建。数据库的连接对象是很重的,而且建立连接需要与数据库服务器通过网络请求建立通信,也让数据库耗费大量资源去响应你的连接请求。总结起来既耗费了计算资源,也浪费网络资源,甚至还浪费了数据库资源。整个创建连接的过程又很耗时,影响了接口的耗时和并发能力。

因此在使用数据库时,以mysql为例,也都会搭配数据库连接池使用。比如最早期的C3P0DBCP到后来的Druid,再到最近的HikariCP。使用的都是同样的数据库,但是搭配上数据库连接池后,性能会有提示。

JMH Benchmarks

线程池

线程池除了简单的复用已存在的线程,可以控制并发之外,还提供了对线程更丰富的控制。

线程池需要合理的配置corePoolSizemaximumPoolSizeworkQueue的容量。较大的队列容量、较小线程池容量,有助于减少cpu使用率。

线程池的参数优化一般有几种方案:

线程池参数方案一

根据项目的特性,如果是CPU密集型,选择N+1的核心线程数。如果是IO密集型,选择2N的核心线程数。

N(cpu核心数):Runtime.getRuntime().availableProcessors()

这种方式优点是粗暴简单,缺点是实际业务场景中往往不适用,而且现实场景中也不会单一的只有一个线程池。

线程池参数方案二

混合型: N*U*(1+WT/ST)

N:CPU核心数
U:目标CPU的利用率
WT:线程等待时间
ST:线程运行时间

线程运行总时间减去自用时间就是等待时间。然而这些时间的计算在实际中很难确认,因此这个公式中由于参数的不准确性,使用起来也很困难。

线程池参数方案三

基于上述线程池优化方案,在网上找到了一篇文章,通过代码的方式,估算出各个参数。

www.javacodegeeks.com/2012/03/threading-stories-about-robust-thread.html

文章中提供一个工具类代码,

/**
 * A class that calculates the optimal thread pool boundaries. It takes the desired target utilization and the desired
 * work queue memory consumption as input and retuns thread count and work queue capacity.
 *
 * @author Niklas Schlimm
 *
 */
public abstract class PoolSizeCalculator {

    /**
     * The sample queue size to calculate the size of a single {@link Runnable} element.
     */
    private final int SAMPLE_QUEUE_SIZE = 1000;

    /**
     * Accuracy of test run. It must finish within 20ms of the testTime otherwise we retry the test. This could be
     * configurable.
     */
    private final int EPSYLON = 20;

    /**
     * Control variable for the CPU time investigation.
     */
    private volatile boolean expired;

    /**
     * Time (millis) of the test run in the CPU time calculation.
     */
    private final long testtime = 3000;

    /**
     * Calculates the boundaries of a thread pool for a given {@link Runnable}.
     *
     * @param targetUtilization
     *            the desired utilization of the CPUs (0 <= targetUtilization <= 1)
     * @param targetQueueSizeBytes
     *            the desired maximum work queue size of the thread pool (bytes)
     */
    protected void calculateBoundaries(BigDecimal targetUtilization, BigDecimal targetQueueSizeBytes) {
        calculateOptimalCapacity(targetQueueSizeBytes);
        Runnable task = creatTask();
        start(task);
        start(task); // warm up phase
        long cputime = getCurrentThreadCPUTime();
        start(task); // test intervall
        cputime = getCurrentThreadCPUTime() - cputime;
        long waittime = (testtime * 1000000) - cputime;
        calculateOptimalThreadCount(cputime, waittime, targetUtilization);
    }

    private void calculateOptimalCapacity(BigDecimal targetQueueSizeBytes) {
        long mem = calculateMemoryUsage();
        BigDecimal queueCapacity = targetQueueSizeBytes.divide(new BigDecimal(mem), RoundingMode.HALF_UP);
        System.out.println("Target queue memory usage (bytes): " + targetQueueSizeBytes);
        System.out.println("createTask() produced " + creatTask().getClass().getName() + " which took " + mem
                + " bytes in a queue");
        System.out.println("Formula: " + targetQueueSizeBytes + " / " + mem);
        System.out.println("* Recommended queue capacity (bytes): " + queueCapacity);
    }

    /**
     * Brian Goetz' optimal thread count formula, see 'Java Concurrency in Practice' (chapter 8.2)
     *
     * @param cpu
     *            cpu time consumed by considered task
     * @param wait
     *            wait time of considered task
     * @param targetUtilization
     *            target utilization of the system
     */
    private void calculateOptimalThreadCount(long cpu, long wait, BigDecimal targetUtilization) {
        BigDecimal waitTime = new BigDecimal(wait);
        BigDecimal computeTime = new BigDecimal(cpu);
        BigDecimal numberOfCPU = new BigDecimal(Runtime.getRuntime().availableProcessors());
        BigDecimal optimalthreadcount = numberOfCPU.multiply(targetUtilization).multiply(
                new BigDecimal(1).add(waitTime.divide(computeTime, RoundingMode.HALF_UP)));
        System.out.println("Number of CPU: " + numberOfCPU);
        System.out.println("Target utilization: " + targetUtilization);
        System.out.println("Elapsed time (nanos): " + (testtime * 1000000));
        System.out.println("Compute time (nanos): " + cpu);
        System.out.println("Wait time (nanos): " + wait);
        System.out.println("Formula: " + numberOfCPU + " * " + targetUtilization + " * (1 + " + waitTime + " / "
                + computeTime + ")");
        System.out.println("* Optimal thread count: " + optimalthreadcount);
    }

    /**
     * Runs the {@link Runnable} over a period defined in {@link #testtime}. Based on Heinz Kabbutz' ideas
     * (http://www.javaspecialists.eu/archive/Issue124.html).
     *
     * @param task
     *            the runnable under investigation
     */
    public void start(Runnable task) {
        long start = 0;
        int runs = 0;
        do {
            if (++runs > 5) {
                throw new IllegalStateException("Test not accurate");
            }
            expired = false;
            start = System.currentTimeMillis();
            Timer timer = new Timer();
            timer.schedule(new TimerTask() {
                public void run() {
                    expired = true;
                }
            }, testtime);
            while (!expired) {
                task.run();
            }
            start = System.currentTimeMillis() - start;
            timer.cancel();
        } while (Math.abs(start - testtime) > EPSYLON);
        collectGarbage(3);
    }

    private void collectGarbage(int times) {
        for (int i = 0; i < times; i++) {
            System.gc();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    /**
     * Calculates the memory usage of a single element in a work queue. Based on Heinz Kabbutz' ideas
     * (http://www.javaspecialists.eu/archive/Issue029.html).
     *
     * @return memory usage of a single {@link Runnable} element in the thread pools work queue
     */
    public long calculateMemoryUsage() {
        BlockingQueue<Runnable> queue = createWorkQueue();
        for (int i = 0; i < SAMPLE_QUEUE_SIZE; i++) {
            queue.add(creatTask());
        }
        long mem0 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        long mem1 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        queue = null;
        collectGarbage(15);
        mem0 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        queue = createWorkQueue();
        for (int i = 0; i < SAMPLE_QUEUE_SIZE; i++) {
            queue.add(creatTask());
        }
        collectGarbage(15);
        mem1 = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        return (mem1 - mem0) / SAMPLE_QUEUE_SIZE;
    }

    /**
     * Create your runnable task here.
     *
     * @return an instance of your runnable task under investigation
     */
    protected abstract Runnable creatTask();

    /**
     * Return an instance of the queue used in the thread pool.
     *
     * @return queue instance
     */
    protected abstract BlockingQueue<Runnable> createWorkQueue();

    /**
     * Calculate current cpu time. Various frameworks may be used here, depending on the operating system in use. (e.g.
     * http://www.hyperic.com/products/sigar). The more accurate the CPU time measurement, the more accurate the results
     * for thread count boundaries.
     *
     * @return current cpu time of current thread
     */
    protected abstract long getCurrentThreadCPUTime();

}

工具使用示例,其中AsynchronousTask即为我们要完成的任务。

public class MyPoolSizeCalculator extends PoolSizeCalculator {

    public static void main(String[] args) {
        MyPoolSizeCalculator calculator = new MyPoolSizeCalculator();
        calculator.calculateBoundaries(
                // CPU目标利用率
                new BigDecimal(1.0),
                // blockingqueue占用大小,单位byte
                new BigDecimal(100000));
    }

    @Override
    protected long getCurrentThreadCPUTime() {
        //当前线程占用的总时间
        return ManagementFactory.getThreadMXBean().getCurrentThreadCpuTime();
    }

    @Override
    protected Runnable creatTask() {
        // 实际执行任务,计算结果;
        // AsynchronousTask 即为需要实现的任务类
        return new AsynchronousTask(0, "IO", 1000000);
    }

    @Override
    protected BlockingQueue createWorkQueue() {
        return new LinkedBlockingQueue<>();
    }

}

线程池参数方案四

正所谓百闻不如一见,能够被可视化的东西还是最让人放心的。如果还能动态调整,实时生效,提供一套可视化管理方案,即让自己便于调试,也让老板安心,这才是中老年程序员生存的终极奥义。

美团技术团队在2020年04月02日有一篇文章,专门谈了动态化线程池的设计方案。并基于此,也自己实现了配置管理功能和线程池监控的功能。

抽空把代码改写一下,修改一下parent的依赖关系,改写一些工具类,再把代码开源出来。

不过大家现在只需要知道可以做到可视化管理并且也已经有实现案例,也可以根据美团文章读懂之后做自己的定制化开发。

美团文章:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

自己的实现只有接口的json返回,没有做可视化展示。扒几张美团的效果图看看美团的可视化成果。


image
image
image

推荐阅读更多精彩内容