享元模式创建连接池
不可变对象的保护性拷贝机制虽然保护了线程安全,但是也会导致对象创建过多的问题,享元模式是不可变对象使用的一种常见设计模式
享元模式就是重用已经存在的对象,如果对象已经存在就不重复创建(最小化内存使用)
在jdk中,通过创建缓存池来实现享元模式
优点:使用原子数组,相当于把锁加在了数组中的每一个元素上,提高了并发度
连接池(测试地址:C:\Users\111\Desktop\java_examples\multi_thread\src\ConnectionPool.java)
每一次请求过来如果都创建一个连接,会使内存压力过大,使用连接池提前创建好连接,请求就来获取连接,请求结束归还连接,实现资源的复用
属性:连接个数(连接池大小),连接对象的数组,表示连接状态的数组
方法:构造函数,获取连接方法,释放连接方法
class Pool{
int poolsize;
Connection[] connections;
//需要使用原子数组来保证线程安全,由这个state数组来间接控制Connection的状态
AtomicIntegerArray state;
//构造函数,初始化属性,并把连接池中的对象创建完毕
public Pool(int poolsize){
this.poolsize=poolsize;
this.connections=new Connectios[poolsize];
this.state=new AtomicIntegerArray(new int[poolsize]);
for(int i=0;i<poolsize;i++){
connections[i]=new MockConnection();
}
}
//获取连接的方法
public Connection getConnection(){
while(true){
//遍历连接状态数组/池,如果有空闲连接就获取连接,否则就阻塞等待
for(int i=0;i<poolsize;i++){
if(state.get(i)==0){
//CAS保证线程安全
if(state.compareAndSet(i,0,1)){
return connections[i];
}
}
}
//循环结束说明找了所有的连接发现都不空闲,这时候需要进行阻塞等待操作
//锁住这个连接池对象,把这个线程放进连接池对象的monitor阻塞队列里
//这里的this就相当于lock,调用lock的wait方法进行阻塞
//在释放连接时进行唤醒
synchornized(this){
try{
this.wait();
}catch(Exception e){
e.print(...);
}
}
}
}
public void release(Connection connection){
for(int i=0;i<poolsize;i++){
if(connections[i]==connection){
//一个连接只能被一个线程所持有,所以这里的连接并不是共享资源,不需要使用compareAndSet
state.set(i,0);
synchornized(this){
this.notifyAll();
}
break;
}
}
}
}为什么在归还连接的时候要检查是否是这个连接池的连接
这是一个非常隐蔽但破坏力极大的问题,被称为 “连接泄露 (Connection Leak)” 的变种,或者叫 “连接污染 (Connection Pollution)”。
后果取决于连接池的具体实现(如 HikariCP, Druid, DBCP),但通常会有以下几种灾难性后果:
1. 状态错乱与数据损坏 (Data Corruption) —— 最严重
假设:
- 池 A(用于支付库,事务隔离级别 Serializable)
- 池 B(用于日志库,事务隔离级别 Read Uncommitted)
如果代码逻辑错误,把 属于池 B 的连接 归还给了 池 A。
后果:
- 池 A 以为这个连接是它自己的,也没做深度检查,就把它标记为“空闲”。
- 下一个业务线程从 池 A 借连接,拿到了这个 “间谍连接”。
- 业务线程以为自己在操作支付库,执行了
UPDATE account SET balance = ...。 - 灾难:SQL 实际执行到了日志库里(报错还算好的),或者因为事务隔离级别不对,导致严重的并发数据不一致。
2. 内存泄漏 (Memory Leak)
- 池 B:因为它的连接没还回来,它以为那个连接还在被使用,一直占着坑。时间久了,池 B 的连接会被耗尽。
- 池 A:因为收到了一个“外来物种”,它维护的
connections列表可能会莫名其妙变大(如果它没做容量限制检查),或者把这个连接当成自己的管理起来。 - 僵尸连接:这个外来连接可能永远不会被关闭,因为它不在池 A 的“心跳检查”名单里,也不在池 B 的回收名单里。
3. 并发报错 (Concurrent Modification / IndexOutOfBounds)
很多简单的连接池实现(如手写连接池),归还逻辑是这样的:
// 简单的归还逻辑
void returnConnection(Connection conn) {
if (this.currentSize < this.maxSize) {
this.connections[currentSize++] = conn; // 直接放回数组
}
}如果归还错了,可能导致:
- 数组越界:如果你不停地把别人的连接还给它,它的连接数可能突破
maxSize。 - 重复归还:同一个连接对象,可能同时存在于 池 A 和 池 B 的空闲队列里。两个不同的线程分别从两个池子里拿到了同一个物理连接,同时操作数据库 —— 这绝对是灾难现场。
工业级连接池是如何防御的?(防御性编程)
优秀的连接池(如 HikariCP)会有防御机制:
1. 包装器模式 (Wrapper)
连接池借给你的从来不是原生 JDBC Connection,而是一个 Proxy 或 Wrapper。 在这个 Wrapper 里,通常藏着一个 parentPool 引用。
// HikariCP 的归还逻辑伪代码
public void close() {
if (this.parentPool != HikariPool.this) {
throw new SQLException("你还错地儿了!这不是我的连接!");
}
// ... 正常归还
}2. 身份标记 (Identity Check)
池子在回收连接时,会检查这个连接对象是否在自己的“白名单”里(比如检查 state 数组对应的引用)。如果发现这个连接根本不在我的记录里,会直接抛出异常或把它关掉。
总结
归还错误的连接就像 “把别人家的钥匙插进了自己家的锁孔”。
- 轻则插不进去(报错)。
- 重则插进去了但打不开(操作失败)。
- 最惨的是插进去还打开了,结果进错了家门,把别人家的东西给砸了(数据污染)。
这是一个非常棒的并发设计问题,触及了高性能并发组件设计的核心哲学:保护元数据(Metadata)而非数据载体(Payload)。
在连接池(Pool)的设计中:
states(状态数组):是元数据,代表“哪个位置能用”。它是多线程竞争的热点,必须绝对线程安全。connections(连接数组):是数据载体,代表“资源本身”。只要状态控制得当,它实际上不需要并发安全保护。
1. 为什么 states 必须是 AtomicIntegerArray?
states 数组扮演的是交通指挥官的角色。 它里面存的是 0 (空闲) 或 1 (占用)。
- 并发场景: 如果有 10 个线程同时来借连接,它们都会疯狂地去扫描
states数组,试图把某个位置的0改成1。 - 竞态条件 (Race Condition): 如果不使用原子类(比如只用普通
int[]),两个线程可能同时看到index=3是0,然后都认为自己抢到了,都去拿connections[3]。 结果:两个线程拿到了同一个 Connection,导致严重的线程安全事故(比如一个正在查 SQL,另一个把连接关了)。 - 解决方案: 使用
AtomicIntegerArray的 CAS 操作(compareAndSet(i, 0, 1))。 只有 CAS 成功的那个线程,才有资格宣布:“这个位置归我了!”
2. 为什么 connections 可以是普通数组?
这就好比 图书馆的借书流程。
states是借阅记录系统:必须原子操作,不能让两个人同时借走同一本书。connections是书架上的书:书本身不需要是“防抢”的。
根本原因:可见性与独占性由 states 保证了。
当线程 A 通过 CAS 成功把 states[i] 从 0 改成 1 后:
- 独占性 (Exclusivity): 其他任何线程再去尝试 CAS
states[i]都会失败。这意味着在 A 释放(归还)之前,只有 A 一个线程能访问connections[i]。 既然只有一个线程访问,自然不存在线程安全问题,普通数组足矣。 - 可见性 (Visibility):
Atomic类的 CAS 操作具有 volatile 的内存语义(Happens-Before 关系)。 A 线程修改状态成功,相当于建立了一个内存屏障。它后续读取connections[i]的操作,一定能看到连接对象是初始化好的(如果是懒加载的话)。
3. 性能考量
AtomicIntegerArray(基于 CAS)比ReentrantLock或synchronized轻量得多,适合高频竞争。- 如果把
connections也做成线程安全的(比如用Vector或CopyOnWriteArrayList),或者给整个数组加锁,那性能会由 O(1) 退化成 O(N) 甚至更差。 - 设计精髓:只对“争抢点”加锁,不对“战利品”加锁。
总结
这个设计的核心在于“护栏逻辑”: 只要你进门的门票(states)检查得足够严格且线程安全,进门之后(访问 connections)其实就是一条单行道,根本不需要额外的线程安全措施。