20、多线程环境下的工厂:线程安全的工厂方法、对象池与工厂模式结合
多线程环境下的工厂模式,说白了就是给工厂模式加把锁。
我刚开始接触并发编程时,觉得工厂模式不就是new个对象嘛,有什么难的?直到线上服务出现OOM,我才意识到——在高并发场景下,对象创建这件事,远比你想象的要复杂。
为什么普通工厂在多线程下会出问题?
先看一个典型的单例工厂实现:
public class ConnectionFactory {
private static ConnectionFactory instance;
public static ConnectionFactory getInstance() {
if (instance == null) { // 线程A和B同时到达这里
instance = new ConnectionFactory(); // 可能创建多个实例
}
return instance;
}
public Connection createConnection() {
return new ConnectionImpl(); // 每次调用都new一个
}
}
这段代码有什么问题?两个线程同时调用getInstance(),instance == null的判断可能同时通过,结果创建了两个实例。嗯,这就是经典的“双重检查锁定”问题。
我在项目中遇到过类似情况,当时是做一个消息推送服务,高峰期每秒要处理上万条连接请求。工厂方法不加控制,结果连接对象满天飞,GC根本来不及回收,最后服务直接挂了。
线程安全的工厂方法实现
解决这个问题,我个人习惯用这几种方式:
1. 使用synchronized关键字
public class SafeConnectionFactory {
private static volatile SafeConnectionFactory instance;
public static SafeConnectionFactory getInstance() {
if (instance == null) {
synchronized (SafeConnectionFactory.class) {
if (instance == null) {
instance = new SafeConnectionFactory();
}
}
}
return instance;
}
public synchronized Connection createConnection() {
return new ConnectionImpl();
}
}
注意这里的volatile关键字,它保证了instance的可见性。我曾经见过有人漏掉volatile,结果在高并发下还是出现了问题。
2. 使用内部静态类(推荐)
public class ConnectionFactory {
private static class Holder {
static final ConnectionFactory INSTANCE = new ConnectionFactory();
}
public static ConnectionFactory getInstance() {
return Holder.INSTANCE;
}
public Connection createConnection() {
return new ConnectionImpl();
}
}
这种方式利用了类加载机制,既保证了线程安全,又实现了懒加载。我个人比较喜欢这种写法,代码简洁,性能也好。
对象池与工厂模式的结合
你想想看,如果每次请求都创建一个新对象,在高并发下会怎样?内存占用飙升,GC频繁触发,响应时间变长。
这时候就需要对象池了。说白了,对象池就是预先创建一批对象,用完了放回去,循环使用。
核心思想:用空间换时间,减少对象创建和销毁的开销。
来看一个线程安全的对象池实现:
public class ConnectionPool {
private final BlockingQueue<Connection> pool;
private final int maxSize;
private final ConnectionFactory factory;
public ConnectionPool(int maxSize, ConnectionFactory factory) {
this.maxSize = maxSize;
this.factory = factory;
this.pool = new LinkedBlockingQueue<>(maxSize);
// 预创建连接
for (int i = 0; i < maxSize / 2; i++) {
pool.offer(factory.createConnection());
}
}
public Connection borrowConnection() throws InterruptedException {
Connection conn = pool.poll();
if (conn == null) {
// 池为空,创建新连接
return factory.createConnection();
}
return conn;
}
public void returnConnection(Connection conn) {
if (conn != null && conn.isValid()) {
pool.offer(conn);
}
}
}
这里用了BlockingQueue,它是线程安全的。我建议你优先使用Java并发包里的工具类,别自己造轮子。
避坑指南:我曾经在对象池里忘记检查连接的有效性,结果返回了一个已经断开的连接,导致业务方报错。所以returnConnection时一定要做有效性校验。
工厂模式+对象池的完整架构
下面这张图展示了工厂模式与对象池结合后的完整流程:
性能对比:普通工厂 vs 带对象池的工厂
我在一个压测项目中做过对比,数据如下:
| 场景 | QPS | 平均响应时间 | GC频率 | 内存占用 |
|---|---|---|---|---|
| 普通工厂 | 1200 | 45ms | 高 | 波动大 |
| 带对象池的工厂 | 3500 | 12ms | 低 | 稳定 |
看到差距了吧?QPS提升了近3倍,响应时间降到了原来的四分之一。这就是对象池的威力。
注意:对象池不是万能的。如果对象创建开销很小,或者对象数量很少,用对象池反而会增加复杂度。我建议你根据实际情况权衡,不要盲目使用。
实际项目中的最佳实践
结合我多年的经验,总结几条实用建议:
- 池大小要合理设置:太小了不够用,太大了浪费内存。一般建议设置为最大并发数的1.5倍。
- 对象要定期清理:长时间不用的对象应该被回收,避免内存泄漏。
- 工厂方法要轻量:工厂里不要做耗时操作,否则会影响对象池的响应速度。
- 考虑使用现成框架:比如Apache Commons Pool2,它已经帮你处理好了线程安全和对象管理。
我记得有一次接手一个遗留系统,里面自己实现了一个对象池,代码写得乱七八糟,各种锁嵌套,性能反而比不用对象池还差。后来我直接换成了Commons Pool2,问题一下子就解决了。
所以,我的建议是:除非你有特殊需求,否则优先使用成熟的第三方库。别重复造轮子,尤其是并发编程这种容易出错的领域。