一些没有经常使用多线程开发的开发者而言,下面的代码可能有点不太容易读懂,主要还是因为不太熟悉这种编程模型,相信各位读者仔细分析代码后,结合代码中注释慢慢分析,如果有不同的地方,欢迎留言。坚持下来,你就会看到不一样的结果!

本文章的重点不在于数据库的使用以及数据库连接的创建,本文着重于使用等待-超时的经典范式来实现多线程下数据库连接池的获取,注意并不注重于数据库的连接的管理等等。

1、等待-超时经典范式

在 线程的状态转换的介绍中,我们知道线程的状态由WAITING状态以及TIMED_WAITING状态,而一般情况下,开发人员大多会使用TIMED_WAITING状态,来在一定时间获取到结果,如果超出时间,那么则返回默认的结果,比如null或其他值。
这种情况,对于之前的等待-通知的经典范式就不太适用了,这种我们采用等待-超时的经典范式模型,其逻辑流程如下:

  1. public synchronized Object get(long timeOut) throws InterruptedException {
  2. // 当前时间+超时时间 = 过期的时间
  3. long future = System.currentTimeMillis() + timeOut;
  4. long remaining = timeOut;
  5. // 如果结果为空或者等待时间未到,则继续wait(remaining)
  6. while (result == null && remaining > 0) {
  7. // 等待通知线程池有新的连接,虽然wait的时间为remaining,但有可能会被中途唤醒
  8. // 如果唤醒后结果仍然为null,并且等待时间仍然大于0,则继续等待当前剩余的时间
  9. wait(remaining);
  10. // 接到通知后继续判断result是不是空的
  11. // 判断剩余的等待时间,如果为负数,说明等待时间已过,超时退出
  12. remaining = future - System.currentTimeMillis();
  13. }
  14. return result;
  15. }

可以看出等待-超时模型就是在等待-通知的基础上增加了超时的控制,这使得线程不会永久的阻塞,非常的灵活。

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、更进一步

上面的代码实例中,数据库的连接数是固定的,一般企业级的数据库连接池都会配置最大连接数和最小连接数,在发现数据库获取连接过多或者过少的情况下,会自动调整可用的连接数,已达到最好的性能,基于此可以尝试从单位时间内观察数据库连接的失败数,如果增多,则增大连接数,但必须小于最大连接数;如果单位时间内,失败数减少,则减少连接数,但必须大于最小连接数。当然这只是一种思路,如果是你的话,你会怎么做?