池技术
日常搬砖过程中对池技术的接触很多,最具代表的是连接池。
连接池也是一种池技术,本质上都是对象池。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 pool;
public ReaderUtil(ObjectPool 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 {
@Override
public StringBuffer create() {
return new StringBuffer();
}
/**
* Use the default PooledObject implementation.
*/
@Override
public PooledObject wrap(StringBuffer buffer) {
return new DefaultPooledObject(buffer);
}
/**
* When an object is returned to the pool, clear the buffer.
*/
@Override
public void passivateObject(PooledObject 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(new StringBufferFactory()));
需要开发关注的仅仅是对象工厂StringBufferFactory的实现,在这个工厂中,主要任务是创建对象,也就是最开始的new对象。把对象的创建工作转移到了工厂里,而不是硬生生的new出来,这也是设计模式的一种体现。
官网给的这个例子非常简洁易懂,很容易快速上手。然而其中还有很多配置参数,能让对象池功能更加丰富。
带配置参数的入门
public void test1() throws InterruptedException {
// 创建池对象工厂
PooledObjectFactory 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 pool = new GenericObjectPool(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 {
@Override
public StringBuilder create() throws Exception {
return new StringBuilder();
}
@Override
public PooledObject 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,到期了就直接“还”回去了,因此这里的输出会将之前的也打印出来,虽然归还了,但是却没清理掉它的内容。
仔细来看,带参数的也就不过如此,对于开发者而言也没有什么太复杂的地方,十分容易上手。接下来就拨开云雾,仔细瞧瞧池技术是如何实现的。