优惠券系统单例ConnectionPool重复实例化致连接翻倍问题求助
问题回顾
我正在开发一个优惠券系统,其中ConnectionPool类采用双重检查锁单例模式设计,首次获取实例时应创建10个数据库连接,并打印10次"Creating connection #num"。我的DBDAO类(比如CompaniesDBDAO)同样为单例,内部持有ConnectionPool的私有实例,但测试时控制台却打印了20次连接创建信息,且ConnectionPool被实例化了2次,特此寻求帮助。
相关代码片段
ConnectionPool 代码
package com.app.utils; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Stack; import com.app.db.DBManager; public class ConnectionPool { private Stack<Connection> connections = new Stack<Connection>(); private static ConnectionPool instance = null; private ConnectionPool() { for (int i = 1; i <= 10; i++) { System.out.println("Creating connection #" + i); try { Connection conn = DriverManager.getConnection(DBManager.getUrl(), DBManager.getUser(), DBManager.getPass()); connections.push(conn); } catch (SQLException e) { System.out.println(e.getMessage()); } } } public static ConnectionPool getInstance() { if (instance == null) { synchronized (ConnectionPool.class) { if (instance == null) { instance = new ConnectionPool(); } } } return instance; } }
CompaniesDBDAO 代码
package com.app.dbdao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import com.app.beans.Company; import com.app.dao.CompaniesDAO; import com.app.utils.ConnectionPool; public class CompaniesDBDAO implements CompaniesDAO { private static CompaniesDBDAO instance = null; private ConnectionPool connectionPool = ConnectionPool.getInstance(); private static final String QUERY_GET_ALL = "SELECT * FROM `jb project`.companies;"; private static final String QUERY_INSERT = "INSERT INTO `jb project`.`companies` (`name`, `email`, `password`) VALUES (?, ?, ?);"; private static final String QUERY_DELETE = "DELETE FROM `jb project`.`companies` WHERE (`id` = ?);"; private static final String QUERY_UPDATE = "UPDATE `jb project`.`companies` SET `name` = ?, `email` = ?, `password` = ? WHERE (`id` = ?);"; private static final String QUERY_GET_ONE = "SELECT * FROM `jb project`.companies WHERE (`id` = ?);"; private CompaniesDBDAO() { } public static CompaniesDBDAO getInstance() { if (instance == null) { synchronized (CompaniesDBDAO.class) { if (instance == null) { instance = new CompaniesDBDAO(); } } } return instance; } }
问题原因分析
你遇到的核心问题是双重检查锁(DCL)单例模式的实现不完整,再加上DAO类的初始化逻辑放大了这个问题:
缺少
volatile修饰符导致指令重排序
在ConnectionPool中,private static ConnectionPool instance = null;没有使用volatile关键字修饰。JVM在实例化对象时会进行指令重排序:new ConnectionPool()的操作会被拆分为内存分配、对象初始化、引用赋值三步。如果没有volatile,JVM可能会把引用赋值的步骤提前到对象初始化之前,导致instance被标记为非null,但对象还未完全初始化。此时如果有其他线程(比如另一个DAO类初始化时)调用getInstance(),会直接返回这个未完全初始化的实例,同时可能因为后续的操作触发再次实例化,最终导致多个ConnectionPool实例被创建。DAO类的成员变量初始化时机
你的每个DBDAO类(比如CompaniesDBDAO、CustomersDBDAO)都在实例变量声明时直接调用ConnectionPool.getInstance():private ConnectionPool connectionPool = ConnectionPool.getInstance();。当多个DAO类在短时间内被初始化时,由于ConnectionPool的单例存在指令重排序问题,每个DAO的初始化都可能触发ConnectionPool的新实例创建。
解决方案
1. 修复ConnectionPool的单例模式(关键)
给instance变量添加volatile修饰符,禁止JVM的指令重排序,确保双重检查锁的正确性:
public class ConnectionPool { private Stack<Connection> connections = new Stack<Connection>(); // 添加volatile修饰符,禁止指令重排序 private static volatile ConnectionPool instance = null; private ConnectionPool() { // 原有初始化逻辑不变 } public static ConnectionPool getInstance() { // 原有双重检查锁逻辑不变 if (instance == null) { synchronized (ConnectionPool.class) { if (instance == null) { instance = new ConnectionPool(); } } } return instance; } }
2. 优化DAO类中ConnectionPool的初始化(可选)
为了避免多个DAO类初始化时的潜在竞争,可以将DAO中的connectionPool改为静态变量,确保只初始化一次:
public class CompaniesDBDAO implements CompaniesDAO { private static CompaniesDBDAO instance = null; // 改为静态变量,类加载时仅初始化一次 private static ConnectionPool connectionPool = ConnectionPool.getInstance(); // ... 其他代码不变 }
或者在DAO的私有构造方法中初始化connectionPool,确保只有当DAO实例被创建时才获取连接池实例:
public class CompaniesDBDAO implements CompaniesDAO { private static CompaniesDBDAO instance = null; private ConnectionPool connectionPool; private CompaniesDBDAO() { // 在构造方法中初始化,延迟到DAO实例创建时 connectionPool = ConnectionPool.getInstance(); } // ... 其他代码不变 }
验证效果
修改后,无论你初始化多少个DAO类,ConnectionPool都只会被实例化一次,控制台只会打印10次"Creating connection #num"的信息。
内容的提问来源于stack exchange,提问作者Tal Levin

