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时一定要做有效性校验。

工厂模式+对象池的完整架构

下面这张图展示了工厂模式与对象池结合后的完整流程:

工厂模式 + 对象池架构图 客户端线程 线程安全工厂 对象池 对象实例 对象实例 对象实例 对象实例 请求创建 获取/归还 管理 创建新对象 流程说明: 1. 客户端请求创建对象 → 2. 工厂检查对象池是否有空闲对象 3. 有则直接返回,无则创建新对象 → 4. 使用完毕后归还到对象池

性能对比:普通工厂 vs 带对象池的工厂

我在一个压测项目中做过对比,数据如下:

场景 QPS 平均响应时间 GC频率 内存占用
普通工厂 1200 45ms 波动大
带对象池的工厂 3500 12ms 稳定

看到差距了吧?QPS提升了近3倍,响应时间降到了原来的四分之一。这就是对象池的威力。

注意:对象池不是万能的。如果对象创建开销很小,或者对象数量很少,用对象池反而会增加复杂度。我建议你根据实际情况权衡,不要盲目使用。

实际项目中的最佳实践

结合我多年的经验,总结几条实用建议:

  • 池大小要合理设置:太小了不够用,太大了浪费内存。一般建议设置为最大并发数的1.5倍。
  • 对象要定期清理:长时间不用的对象应该被回收,避免内存泄漏。
  • 工厂方法要轻量:工厂里不要做耗时操作,否则会影响对象池的响应速度。
  • 考虑使用现成框架:比如Apache Commons Pool2,它已经帮你处理好了线程安全和对象管理。

我记得有一次接手一个遗留系统,里面自己实现了一个对象池,代码写得乱七八糟,各种锁嵌套,性能反而比不用对象池还差。后来我直接换成了Commons Pool2,问题一下子就解决了。

所以,我的建议是:除非你有特殊需求,否则优先使用成熟的第三方库。别重复造轮子,尤其是并发编程这种容易出错的领域。


公众号:蓝海资料掘金营,微信deep3321