池技术使用-commons-pool2

2020-07-06  本文已影响0人  MR丿VINCENT

池技术

日常搬砖过程中对池技术的接触很多,最具代表的是连接池。
连接池也是一种池技术,本质上都是对象池。commons-pool是apacha基金会开源的一款常见的对象池工具库。

使用池化主要是为了节省对象创建的开销。比如日常开发息息相关的数据源连接池,就是为了减少连接创建的时间而生的。可以简单评估一下一个连接的创建经历哪些操作:对象创建,tcp连接等。tcp连接又得经历三次握手,如果是tls/ssl还得做证书签名验证,想想都麻烦。所以使用连接池可以减少这些消耗性能的操作,把机器更多的性能留给业务。

快速上手

这里直接搬运官网的demo。

下面是一个从流中读取字符串的工具类。

import java.io.Reader; 
import java.io.IOException; 
 
public class ReaderUtil { 
    public ReaderUtil() { 
    } 
 
    /** 
     * Dumps the contents of the {@link Reader} to a 
     * String, closing the {@link Reader} when done. 
     */ 
    public String readToString(Reader in) throws IOException { 
        StringBuffer buf = new StringBuffer(); 
        try { 
            for(int c = in.read(); c != -1; c = in.read()) { 
                buf.append((char)c); 
            } 
            return buf.toString(); 
        } catch(IOException e) { 
            throw e; 
        } finally { 
            try { 
                in.close(); 
            } catch(Exception e) { 
                // ignored 
            } 
        } 
    } 
}

咋看上去没什么毛病,我们在日常搬砖中也会写出这样的工具类,也可以很好的工作。为了突出说明池化技术的优点,这个工具类还能继续优化,虽然优化空间不是很大。

import java.io.IOException;
import java.io.Reader;
import org.apache.commons.pool2.ObjectPool;

public class ReaderUtil {
    
    private ObjectPool<StringBuffer> pool;
    
    public ReaderUtil(ObjectPool<StringBuffer> pool) {
        this.pool = pool;
    }

    /**
     * Dumps the contents of the {@link Reader} to a String, closing the {@link Reader} when done.
     */
    public String readToString(Reader in)
        throws IOException {
        StringBuffer buf = null;
        try {
            buf = pool.borrowObject();
            for (int c = in.read(); c != -1; c = in.read()) {
                buf.append((char) c);
            }
            return buf.toString();
        } catch (IOException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("Unable to borrow buffer from pool" + e.toString());
        } finally {
            try {
                in.close();
            } catch (Exception e) {
                // ignored
            }
            try {
                if (null != buf) {
                    pool.returnObject(buf);
                }
            } catch (Exception e) {
                // ignored
            }
        }
    }
}

明眼人很快就能看出区别,无非就是将StringBuffer的创建方式做了变化,以前是直接new,每次调用都得new一下,现在是通过向pool借。

import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

public class StringBufferFactory
    extends BasePooledObjectFactory<StringBuffer> {

    @Override
    public StringBuffer create() {
        return new StringBuffer();
    }

    /**
     * Use the default PooledObject implementation.
     */
    @Override
    public PooledObject<StringBuffer> wrap(StringBuffer buffer) {
        return new DefaultPooledObject<StringBuffer>(buffer);
    }

    /**
     * When an object is returned to the pool, clear the buffer.
     */
    @Override
    public void passivateObject(PooledObject<StringBuffer> pooledObject) {
        pooledObject.getObject().setLength(0);
    }

    // for all other methods, the no-op implementation
    // in BasePooledObjectFactory will suffice
}

最终只需要将pool传给这个util:

ReaderUtil readerUtil = new ReaderUtil(new GenericObjectPool<StringBuffer>(new StringBufferFactory()));

需要开发关注的仅仅是对象工厂StringBufferFactory的实现,在这个工厂中,主要任务是创建对象,也就是最开始的new对象。把对象的创建工作转移到了工厂里,而不是硬生生的new出来,这也是设计模式的一种体现。

官网给的这个例子非常简洁易懂,很容易快速上手。然而其中还有很多配置参数,能让对象池功能更加丰富。

带配置参数的入门

public void test1() throws InterruptedException {
        // 创建池对象工厂
        PooledObjectFactory<StringBuilder> factory = new MyPoolableObjectFactory();

        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        // 最大空闲数
        poolConfig.setMaxIdle(5);
        // 最小空闲数, 池中只有一个空闲对象的时候,池会在创建一个对象,并借出一个对象,从而保证池中最小空闲数为1
        poolConfig.setMinIdle(1);
        // 最大池对象总数
        poolConfig.setMaxTotal(20);
        // 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)
        poolConfig.setMinEvictableIdleTimeMillis(1800000);
        // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
        poolConfig.setTimeBetweenEvictionRunsMillis(1800000 * 2L);
        // 在获取对象的时候检查有效性, 默认false
        poolConfig.setTestOnBorrow(true);
        // 在归还对象的时候检查有效性, 默认false
        poolConfig.setTestOnReturn(false);
        // 在空闲时检查有效性, 默认false
        poolConfig.setTestWhileIdle(false);
        // 最大等待时间, 默认的值为-1,表示无限等待。
        poolConfig.setMaxWaitMillis(6000);
        // 是否启用后进先出, 默认true
        poolConfig.setLifo(true);
        // 连接耗尽时是否阻塞, false报异常,true阻塞直到超时, 默认true
        poolConfig.setBlockWhenExhausted(true);
        // 每次逐出检查时 逐出的最大数目 默认3
        poolConfig.setNumTestsPerEvictionRun(3);

        CountDownLatch latch = new CountDownLatch(40);
        // 创建对象池
        final GenericObjectPool<StringBuilder> pool = new GenericObjectPool<StringBuilder>(factory, poolConfig);
        for (int i = 0; i < 40; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    StringBuilder resource = null;
                    try {
                        // 注意,如果对象池没有空余的对象,那么这里会block,可以设置block的超时时间
                        resource = pool.borrowObject();
                        resource.append("+").append(finalI);
                        System.out.println(resource);
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        // 申请的资源用完了记得归还,不然其他人要申请时可能就没有资源用了
                        pool.returnObject(resource);
                        latch.countDown();
                    }
                }
            }).start();

        }
        latch.await();

        System.out.println("=====finish====");
    }
    
    private static class MyPoolableObjectFactory extends BasePooledObjectFactory<StringBuilder> {

        @Override
        public StringBuilder create() throws Exception {
            return new StringBuilder();
        }

        @Override
        public PooledObject<StringBuilder> wrap(StringBuilder obj) {
            return new DefaultPooledObject<>(obj);
        }
    }

这个demo中给了很多配置参数,注释中写的都很明白。值得注意的是这个demo中输出的结果可能不一致。因为多线程的缘故。

下面是其中的一种输出结果:

+5
+9
+8
+11
+12
+10
+4
+1
+3
+2
+7
+13
+15
+16
+6
+17
+18
+19
+0
+14
+3+20
+9+21
+11+22
+4+25
+2+23
+5+24
+1+26
+0+27
+14+28
+9+21+29
+3+20+30
+4+25+31
+1+26+32
+2+23+33
+11+22+35
+5+24+34
+0+27+36
+14+28+37
+3+20+30+38
+9+21+29+39
=====finish====

这里开了40个线程去获取对象,通过使用latch使得所有线程都结束后再结束主线程。
这个latch得控制为40,因为每个线程跑完都得减一,直到为0后表示所有线程都结束。这里都latch只是用于控制先后顺序,也就是即使主线程结束了,子线程也能继续执行下去,除非子线程都是守护线程。

由于设置都最大数量为20,因此会有20个线程先获取到stringbuffer对象,然后这里睡眠了2秒钟,模拟一下对这个对象的使用,剩下的20个线程会尝试去“借”对象,但是之前的20个线程还没用完,因此不会马上获取到,这里设置了一个超时时间6s,也就是最多等6s,如果6s后还是没能等到,那就直接抛异常了。因为模拟只使用2s,到期了就直接“还”回去了,因此这里的输出会将之前的也打印出来,虽然归还了,但是却没清理掉它的内容。

仔细来看,带参数的也就不过如此,对于开发者而言也没有什么太复杂的地方,十分容易上手。接下来就拨开云雾,仔细瞧瞧池技术是如何实现的。

上一篇下一篇

猜你喜欢

热点阅读