一些没有经常使用多线程开发的开发者而言,下面的代码可能有点不太容易读懂,主要还是因为不太熟悉这种编程模型,相信各位读者仔细分析代码后,结合代码中注释慢慢分析,如果有不同的地方,欢迎留言。坚持下来,你就会看到不一样的结果!
本文章的重点不在于数据库的使用以及数据库连接的创建,本文着重于使用等待-超时的经典范式来实现多线程下数据库连接池的获取,注意并不注重于数据库的连接的管理等等。
1、等待-超时经典范式
在 线程的状态转换的介绍中,我们知道线程的状态由WAITING状态以及TIMED_WAITING状态,而一般情况下,开发人员大多会使用TIMED_WAITING状态,来在一定时间获取到结果,如果超出时间,那么则返回默认的结果,比如null或其他值。
这种情况,对于之前的等待-通知的经典范式就不太适用了,这种我们采用等待-超时的经典范式模型,其逻辑流程如下:
public synchronized Object get(long timeOut) throws InterruptedException {// 当前时间+超时时间 = 过期的时间long future = System.currentTimeMillis() + timeOut;long remaining = timeOut;// 如果结果为空或者等待时间未到,则继续wait(remaining)while (result == null && remaining > 0) {// 等待通知线程池有新的连接,虽然wait的时间为remaining,但有可能会被中途唤醒// 如果唤醒后结果仍然为null,并且等待时间仍然大于0,则继续等待当前剩余的时间wait(remaining);// 接到通知后继续判断result是不是空的// 判断剩余的等待时间,如果为负数,说明等待时间已过,超时退出remaining = future - System.currentTimeMillis();}return result;}
可以看出等待-超时模型就是在等待-通知的基础上增加了超时的控制,这使得线程不会永久的阻塞,非常的灵活。
2、基于等待-超时模型实现数据库连接池
基于等到-超时模型,我们来实现JavaEE项目中经常实现的数据库连接池,鉴于篇幅问题,这里仅仅简单实现,并非真正的实现一款可应用的数据库连接池,将偏重于线程的使用。
实例的需求为子线程尝试从数据库连接池中获取连接,如果在1000秒内获取不到,则返回获取失败,设定数据库连接池的可用大小为20个连接,设置10个线程,尝试获取连接,观察获取连接成功的次数和获取失败的连接的次数。
2.1 定义数据库连接池
连接池的定义如下: 通过构造函数初始化连接池,创建若干个连接,通过一个双向的链表来维护,最后,线程通过get(long timeout) 方法来回去数据库连接。
import java.sql.Connection;
import java.util.LinkedList;
public class ConnectionPool {
private final LinkedList<Connection> pool;
// 私有化构造函数,创造的时候用于初始化连接
private ConnectionPool(int initPoolSize) {
pool = new LinkedList<>();
if (initPoolSize > 0) {
for (int i = 0; i < initPoolSize; i++) {
pool.addLast(ConnectDriver.createConnection());
}
}
}
// 初始化连接池
public static ConnectionPool getConnectionPool(int initPoolSize) {
return new ConnectionPool(initPoolSize);
}
// 释放连接,如果释放的连接不为空,则添加到链表的尾部
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (pool) {
pool.addLast(connection);
pool.notify();
}
}
}
// 获取连接
public Connection get(long timeOut) throws InterruptedException {
synchronized (pool) {
long future = System.currentTimeMillis() + timeOut;
long remaining = timeOut;
while (pool.isEmpty() && remaining > 0) {
// 等待通知线程池有新的连接
pool.wait(remaining);
// 发现连接后继续判断,连接池是不是空的,此时连接池应当不为空
// 连接池不为空,则跳出循环,准备获取连接
remaining = future - System.currentTimeMillis();
}
Connection connection = null;
if (!pool.isEmpty()) {
connection = pool.removeFirst();
}
return connection;
}
}
}
代码方面也比较简单,主要是get(long timeOut)方法,重点在这个等待-超时模型上,需要认真查询,至于初始化代码中的创建Connection,这这里我们使用动态代理实现,为了实现耗时的效果,这里在commit的时候延时100ms,模型SQL执行的耗时效果。
public class ConnectDriver {
private static final ClassLoader classLoader = ConnectDriver.class.getClassLoader();
static class ConnectHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果是commit方法,延时100ms,其他方法直接返回null
if (method.getName().equalsIgnoreCase("commit")) {
TimeUnit.MILLISECONDS.sleep(100);
}
return null;
}
}
// 静态方法,通过动态代理获取Connection的代理对象
public static Connection createConnection() {
return (Connection)
Proxy.newProxyInstance(
classLoader, new Class<?>[] {Connection.class}, new ConnectHandler());
}
}
2.2 数据库连接池使用示例
数据库连接池定义好之后,我们来写个多线程的示例,生成20个线程,每个线程尝试获取数据库连接30次,并统计获取的结果。
public class ConnectionUsage {
// 定义数据库连接池,初始化20个数据库连接
static ConnectionPool pool = ConnectionPool.getConnectionPool(10);
// 开始标记,用用于创建30个线程之后统一开始
static CountDownLatch startLatch = new CountDownLatch(1);
// 定义结束标记,每个线程完成任务之后减一,直到减完,程序执行结束,输出结果
static CountDownLatch endLatch = new CountDownLatch(30);
public static void main(String[] args) throws InterruptedException {
// 定义获取到成功的个数以及失败的个数
AtomicInteger getCount = new AtomicInteger();
AtomicInteger notGetCount = new AtomicInteger();
// 定义30个线程,并开始执行
for (int i = 0; i < 30; i++) {
Thread thread = new Thread(new ConnectRunner(startLatch, endLatch, getCount, notGetCount));
thread.start();
}
// 开始执行所有线程
startLatch.countDown();
// 等待所有线程被执行完
endLatch.await();
System.out.println("共计获取: 600");
System.out.println("获取成功: " + getCount.intValue());
System.out.println("获取失败: " + notGetCount.intValue());
}
static class ConnectRunner implements Runnable {
// 开始信号
CountDownLatch startLatch;
// 结束信号
CountDownLatch endLatch;
// 成功数
AtomicInteger getCount;
// 失败数
AtomicInteger notGetCount;
// 每个线程获取的数量
int count = 20;
public ConnectRunner(
CountDownLatch startLatch,
CountDownLatch endLatch,
AtomicInteger getCount,
AtomicInteger notGetCount) {
this.startLatch = startLatch;
this.endLatch = endLatch;
this.getCount = getCount;
this.notGetCount = notGetCount;
}
@Override
public void run() {
// 等待同一时间开始
try {
startLatch.await();
} catch (InterruptedException ignore) {
ignore.printStackTrace();
}
// 循环获取count次连接
while (count > 0) {
try {
Connection connection = pool.get(1000);
if (connection != null) {
// 模拟执行数据库操作,执行完成之后,释放连接,并使成功数加1
try {
connection.createStatement();
connection.commit();
} finally {
pool.releaseConnection(connection);
getCount.incrementAndGet();
}
} else {
// 如果获取失败,失败数加1
notGetCount.incrementAndGet();
}
} catch (Exception ignore) {
} finally {
count--;
}
}
// 执行count次之后,endLatch减少1
endLatch.countDown();
}
}
}
输出结果如下,读者可自行实现数据库连接池以及调整线程数和最大数据库连接数,观察不同线程和不同连接数的情况获取成功和失败的比例,将会发现,随着线程的增多,获取数据库连接的失败比率会慢慢增多。其实在多线程的情况下,添加超时机制,按时返回,这其实也是操作系统的一种保护机制。
共计获取: 600
获取成功: 543
获取失败: 57
3、更进一步
上面的代码实例中,数据库的连接数是固定的,一般企业级的数据库连接池都会配置最大连接数和最小连接数,在发现数据库获取连接过多或者过少的情况下,会自动调整可用的连接数,已达到最好的性能,基于此可以尝试从单位时间内观察数据库连接的失败数,如果增多,则增大连接数,但必须小于最大连接数;如果单位时间内,失败数减少,则减少连接数,但必须大于最小连接数。当然这只是一种思路,如果是你的话,你会怎么做?
