享元模式
1. 使用场景
当很多线程需要重用数量有限的同一类对象时。
例如:一个线上商城应用,QPS达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。
这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库。
2. 实现举例
class Pool {
// 连接池大小
private int poolSize;
// 连接数组
private Connection[] conns;
// 连接是否被占用标记
private AtomicBoolean[] locks;
// 初始化连接池
public Pool(int poolSize) {
this.poolSize = poolSize;
conns = new Connection[poolSize];
locks = new AtomicBoolean[poolSize];
for (int i = 0; i < poolSize; i++) {
conns[i] = new SomeConnection("conn" + (i + 1));
locks[i] = new AtomicBoolean(false);
}
}
// 从连接池暂借连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
// 如果空闲,修改占用标记,并返回连接
if (locks[i].get() == false) {
locks[i].compareAndSet(false, true);
return conns[i];
}
}
// 所有连接都繁忙,则需等待空闲连接
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
}
}
}
}
// 将使用完毕的连接还回连接池
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (conns[i] == conn) {
locks[i].compareAndSet(true, false);
// 唤醒等待空闲连接的线程
synchronized (this) {
this.notifyAll();
}
}
}
}
}
class SomeConnection implements Connection {
// 实现略
}
测试使用:
Pool pool = new Pool(10);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Connection conn = pool.borrow();
System.out.println(Thread.currentThread().getName() + " " + conn);
try {
Thread.sleep(new Random().nextInt(1000));
} catch (Exception e) { }
pool.free(conn);
}).start();
}
3. 更佳实现
以上实现没有考虑:
- 连接的动态增长与收缩
- 连接保活(可用性检测)
- 等待超时处理
- 分布式hash
对于关系型数据库,有比较成熟的连接池实现,例如c3p0, druid datasource等
对于更通用的对象池,可以考虑使用apache commons pool
如果是要连接支持分布式的内存数据库,如redis,那么可以参考jedis中关于连接池的实现
4. 另一类享元
在JDK中 Integer,Long等包装类提供了valueOf方法,例如Long的valueOf会缓存-128~127之间的Long对象,在这个范围之间会重用对象,大于这个范围,才会新建Long对象:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}