线程池实战——数据库连接池

news2025/5/29 8:29:31

引言

作者在前面写了很多并发编程知识深度探索系列文章,反馈得知友友们收获颇丰,同时我也了解到友友们也有了对知识如何应用感到很模糊的问题。所以作者就打算写一个实战系列文章,让友友们切身感受一下怎么应用知识。话不多说,开始吧!

在当今数据驱动的时代,数据库作为应用程序的核心组成部分,其连接的高效管理直接影响着系统的性能与稳定性。尤其是在高并发场景下,频繁创建和销毁数据库连接会带来巨大的资源开销,严重制约系统的响应速度和吞吐量。数据库连接池技术应运而生,通过预先创建并管理一定数量的连接,实现连接的复用,极大提升了资源利用效率。本文将通过一个基于 Java 的示例,深入剖析如何运用 CountDownLatch、等待超时模式与动态代理等技术,构建线程安全的数据库连接池,为高并发环境下的资源管理提供清晰的实践路径与解决方案。​

实战前置知识

1.CountDownLatch

CountDownLatch 是 JUC 中的一个同步工具类,用于协调多个线程之间的同步,确保主线程在多个子线程完成任务后继续执行。下面这篇博客文章也有讲过《并发工具类》CountDownLatch 并发编程:各种锁机制、锁区别、并发工具类深刻总结-CSDN博客

CountDownLatch 它的核心思想是通过一个倒计时计数器来控制多个线程的执行顺序。

class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    Thread.sleep((long) (Math.random() * 1000)); // 模拟任务执行
                    System.out.println(Thread.currentThread().getName() + " 执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 线程完成后,计数器 -1
                }
            }).start();
        }

        latch.await(); // 主线程等待
        System.out.println("所有子线程执行完毕,主线程继续执行");
    }
}

在使用的时候,我们需要先初始化一个 CountDownLatch 对象,指定一个计数器的初始值,表示需要等待的线程数量。然后在每个子线程执行完任务后,调用 countDown() 方法,计数器减 1。接着主线程调用 await() 方法进入阻塞状态,直到计数器为 0,也就是所有子线程都执行完任务后,主线程才会继续执行。

例如《秦二爷:王者荣耀等待玩家确认》例子。

以王者荣耀为例,我们来创建五个线程,分别代表大乔、兰陵王、安其拉、哪吒和铠。每个玩家都调用 countDown() 方法,表示已就位。主线程调用 await() 方法,等待所有玩家就位。

2.等待超时模式

2.1等待/通知模式的经典范式

等待/通知的经典范式,该范式分为两部分,分别针对等待方(消费者)和通知方(生产者)。

等待方遵循如下原则。

  1. 获取对象的锁。
  2. 如果条件不满足,那么调用对象的 wait()方法,被通知后仍要检查条件
  3. 条件满足则执行对应的逻辑。

对应的伪代码如下。

synchronized (对象) {
    while (条件步满足) {
        对象.wait();
    }
    对应的处理逻辑
}

通知方遵循如下原则。

  1. 获得对象的锁。
  2. 改变条件。
  3. 通知所有等待在对象上的线程。

对应的伪代码如下

synchronized(对象) {
    改变条件
    对象.notifyAll();
}

2.2等待超时模式

开发人员经常会遇到这样的方法调用场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段得到结果,那么立刻将结果返回,反之超时返回默认结果。

前面介绍了等待/通知的经典范式,即加锁、条件循环和处理逻辑3个步骤,但这种范式无法做到超时等待。要想支持超时等待,只需要对经典范式做非常小的改动,改动内容如下所示。

假设超时时间段是T,那么可以推断出在当前时间now+T之后就会超时。定义如下变量口。

  1. 等待持续时间:REMAINING=T。
  2. 超时时间:FUTURE=now+T。

这时仅需要执行 wait(REMAINING),在 wait(REMAINING)返回后将执行REMAINING=FUTURE-NOW。如果REMAINING小于或等于0,表示已经超时,直接退出,否则将继续执行 wait(REMAINING)。
上述描述等待超时模式的伪代码如下。

//对当前对象加锁
public synchronized Object get(long mills) throws InterruptedException {
    long future = System.currentTimeMillis() + mills;
    long remaining = mills;
    //超时大于0并且result返回值不满足要求
    while ((result == null) && remaining > 0) {
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }
    return result;
}
 

可以看出,等待超时模式就是在等待/通知的经典范式的基础上增加了超时控制,这使得该模式相比原有范式更具灵活性,因为即使方法的执行时间过长,也不会“永久”阻塞调用者,而是会按照调用者的要求“按时”返回。

 实战开始—>数据库连接池示例

我们使用等待超时模式来构造一个简单的数据库连接池,模拟从连接池中获取、使用和释放连接的过程,而客户端获取连接的过程被设定为等待超时的模式,也就是在1000ms内如果无法获取到可用连接,将会返回给客户端一个null。设定连接池的大小为10个,然后通过调节客户端的线程数来模拟无法获取连接的场景。
首先看一下连接池的定义。它通过构造函数初始化连接的最大上限,通过一个双向队列来维护连接,调用着需要先调用fetchConnection(long)方法来指定在多少ms内超时获取连接、当连接使用完成后,需要调用releaseConnection(Conneetion)方法将连接放回线程池。
示例代码如下

public class ConnectionPool {
    private LinkedList<Connection> pool = new LinkedList<Connection>();

    public ConnectionPool(int initialSize) {
        if (initialSize <= 0) throw new IllegalArgumentException();
        for (int i = 0; i < initialSize; i++) {
            pool.addLast(ConnectionDriver.createConnection());
        }
    }

    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool) {
                pool.addLast(connection);
                pool.notifyAll();
            }
        }
    }

    public Connection fetchConnection(long mills) throws InterruptedException {
        synchronized (pool) {
            if (mills <= 0) {
                while (pool.isEmpty()) {
                    pool.wait();
                }
                return pool.removeFirst();
            } else {
                long future = System.currentTimeMillis() + mills;
                long remaining = mills;
                while (pool.isEmpty() && remaining > 0) {
                    pool.wait(remaining);
                    remaining = future - System.currentTimeMillis();
                }
                Connection result = null;
                if (!pool.isEmpty()) {
                    result = pool.removeFirst();
                }
                return result;
            }
        }
    }
}
 

由于java.sql.Connection是一个接口,最终的实现是由数据库驱动提供方来实现的,考虑到这只是个示例,我们通过动态代理构造了一个Connection,该Connection的代理实现仅仅是在commitO)方法调用时休眠100ms,示例如下。

public class ConnectionDriver {
    static class ConnectionHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getName().equals("commit")) {
                TimeUnit.MILLISECONDS.sleep(100);
            }
            return null;
        }
    }

    public static final Connection createConnection() {
        return (Connection) Proxy.newProxyInstance(
            ConnectionDriver.class.getClassLoader(),
            new Class<?>[] { Connection.class },
            new ConnectionHandler()
        );
    }
}
 

下面通过一个示例来测试简易数据库连接池的工作情况。模拟客户端ConnectionRunner获取、使用、释放连接的过程,当它使用时将会增加获取到的连接的数量,反之,将会增加未获取到的连接的数量,示例如下。

public class ConnectionPoolTest {
    static ConnectionPool pool = new ConnectionPool(10);
    static CountDownLatch start = new CountDownLatch(1);
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
        int threadCount = 10;
        end = new CountDownLatch(threadCount);
        int count = 20;
        AtomicInteger got = new AtomicInteger();
        AtomicInteger notGot = new AtomicInteger();

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new ConnectionRunner(count, got, notGot), "ConnectionRunnerThread");
            thread.start();
        }
        start.countDown();
        end.await();
        System.out.println("total invoke: " + (threadCount * count));
        System.out.println("got connection: " + got);
        System.out.println("not got connection " + notGot);
    }

    static class ConnectionRunner implements Runnable {
        int count;
        AtomicInteger got;
        AtomicInteger notGot;

        public ConnectionRunner(int count, AtomicInteger got, AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run() {
            try {
                start.await();
            } catch (Exception ex) {
            }
            while (count > 0) {
                try {
                    Connection connection = pool.fetchConnection(1000);
                    if (connection != null) {
                        try {
                            connection.createStatement();
                            connection.commit();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}
 

上述示例中使用了CountDownLatch来确保ConnectionRunnerThread能够同时开始执并且在全部结束之后,才使main线程从等待状态中返回。当前设定的场景是10个线同时运行来获取连接池(10个连接)中的连接,通过调节线程数量来观察未获取到的连的情况。线程数量、总获取次数、获取到的数量、未获取到的数量以及未获取到的比率如表所示。不同电脑机器实际输出可能与此表不同。

线程数量

总获取次数

获取到的数量

未获取到的数量

未获取到的比率

10

200

200

0

0%

20

400

387

13

3.25%

30

600

542

58

9.67%

40

800

700

100

12.5%

50

1000

828

172

17.2%

从表中的数据统计可以看出,在资源一定的情况下(连接池中的10个连接),随着客户端线程的逐步增加,客户端出现超时无法获取连接的比率不断升高。虽然客户端线程在这种超时获取的模式下会出现连接无法获取的情况,但是它能够保证客户端线程不会一直挂在连接获取的操作上,而是“按时”返回,并告知客户端连接获取出现问题,这是系统的一种自我保护机制。数据库连接池的设计也可以复用到其他的资源获取场景,针对昂贵资源(比如数据库连接)的获取都应该进行超时限制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2386172.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于moonshot模型的Dify大语言模型应用开发核心场景

基于moonshot模型的Dify大语言模型应用开发核心场景学习总结 一、Dify环境部署 1.Docker环境部署 这里使用vagrant部署&#xff0c;下载vagrant之后&#xff0c;vagrant up登陆&#xff0c;vagrant ssh&#xff0c;在vagrant 中使用 vagrant centos/7 init 快速创建虚拟机 安装…

华为OD机试真题——字符串序列判定(2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 B卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

工商总局可视化模版-Echarts的纯HTML源码

概述 基于ECharts的工商总局数据可视化HTML模版&#xff0c;帮助开发者快速搭建专业级工商广告数据展示平台。这款模版设计规范&#xff0c;功能完善&#xff0c;适合各类工商监管场景使用。 主要内容 本套模版采用现代化设计风格&#xff0c;主要包含以下核心功能模块&…

Spring AI 和 Elasticsearch 作为你的向量数据库

作者&#xff1a;来自 Elastic Josh Long, Philipp Krenn 及 Laura Trotta 使用 Spring AI 和 Elasticsearch 构建一个完整的 AI 应用程序。 Elasticsearch 原生集成了业界领先的生成式 AI 工具和服务提供商。查看我们关于超越 RAG 基础或使用 Elastic 向量数据库构建生产级应用…

集群聊天服务器学习 配置开发环境(VScode远程连接虚拟机Linux开发)(2)

配置远程开发环境 第一步&#xff1a;Linux系统运行sshd服务 第二步&#xff1a;在vscode上安装Remote Deve I opment插件&#xff0c;其依赖插件会自动安装 第三步&#xff1a;配置远程Linux主机的信息 第四步&#xff1a;在vscode上开发远程连接Linux 第一步&#xff1a;…

rabbitmq的使用介绍

一.队列工作模式介绍 1.WorkQueues模型 生产者直接把消息发送给队列&#xff0c;然后消费者订阅队列 特点: 消息不会重复, 分配给不同的消费者. 代码实现&#xff1a; 消费者代码&#xff1a; Component Slf4j public class SpringRabbitListener {RabbitListener(queues &q…

系统编程day04

一.进程的基本概念 一.定义 进程是一个程序执行的过程&#xff08;也可以说是正在运行的程序&#xff09;&#xff0c;是系统分配资源的基本单位&#xff0c;由cpu对各个进程指挥调度&#xff0c;在单核cpu的情况下,各个进程可以通过一定规则在cpu上并发运行。 二.PCB块 1.PC…

Arduino Uno KY-037声音传感器实验

KY-037声音传感器实验 KY-037声音传感器实验1、 实验内容2、KY-037声音传感器介绍3、实验注意事项4、代码和实验现象 KY-037声音传感器实验 1、 实验内容 通过对KY-037声音传感器吹气&#xff0c;控制LED的打开和关闭&#xff0c;吹一下LED打开&#xff0c;在吹一下LED关闭。…

基于音频Transformer与动作单元的多模态情绪识别算法设计与实现(在RAVDESS数据集上的应用)

摘要&#xff1a;情感识别技术在医学、自动驾驶等多个领域的广泛应用&#xff0c;正吸引着研究界的持续关注。本研究提出了一种融合语音情感识别&#xff08;SER&#xff09;与面部情感识别&#xff08;FER&#xff09;的自动情绪识别系统。在SER方面&#xff0c;我们采用两种迁…

什么是VR实景?有哪些高价值场景?

在数字化浪潮的推动下&#xff0c;虚拟现实技术正以前所未有的速度改变着我们的生活方式和工作模式。 其中&#xff0c;VR实景作为VR技术的一个重要应用场景&#xff0c;独特的沉浸感和交互性&#xff0c;在众多领域展现出应用潜力和高价值场景。什么是VR实景&#xff1f;VR实…

同一无线网络下的设备IP地址是否相同?

在家庭和办公网络普及的今天&#xff0c;许多人都会好奇&#xff1a;连接同一个Wi-Fi的设备是否共享相同的IP地址&#xff1f;这个问题看似简单&#xff0c;实则涉及多个角度。本文将为您揭示其中的技术奥秘。 用一个无线网IP地址一样吗&#xff1f;同一无线网络&#xff08;如…

第2周 PINN核心技术揭秘: 如何用神经网络求解偏微分方程

1. PDEs与传统数值方法回顾 (Review of PDEs & Traditional Numerical Methods) 1.1 什么是偏微分方程 (Partial Differential Equations, PDEs)? 偏微分方程是描述自然界和工程领域中各种物理现象(如热量传播、流体流动、波的振动、电磁场分布等)的基本数学语言。 1.…

【C语言】习题练手套餐 2

每日习题分享。 字符串函数的运用 首先回顾一下字符串函数。 字符串长度 strlen(const char *s);功能&#xff1a;计算字符串的长度&#xff0c;不包含终止符\0。 字符串连接 char *strcat(char *dest, const char *src); char *strncat(char *dest, const char *src, si…

[项目总结] 基于Docker与Nginx对项目进行部署

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

神经正切核推导(2)

对神经正切核的理解和推导&#xff08;1&#xff09;-CSDN博客 这篇文章包括很多概念的理解 声明&#xff1a; 本篇文章来自于Neural Tangent Kernel &#xff08;NTK&#xff09;基础推导 - Gearlesskai - 博客园 旨在对上述推导过程进行再推导与理解 手写推导部分与其他颜…

Ctrl+鼠标滚动阻止页面放大/缩小

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 一般在我们做大屏的时候&#xff0c;不希望Ctrl鼠标上下滚动的时候页面会放大/缩小&#xff0c;那么在有时候&#xff0c;又不希望影响到别的页面&#xff0c;比如说这个大屏是在另一个管理后台中&am…

3d世界坐标系转屏幕坐标系

世界坐标 ——> NDC标准设备坐标 ——> 屏幕坐标 标准设备NDC坐标系 屏幕坐标系 .project方法将 将向量(坐标)从世界空间投影到相机的标准化设备坐标 (NDC) 空间。 手动实现HTML元素定位到模型位置&#xff0c;实现模型标签效果&#xff08;和css2Render原理同理&#…

【2025】基于Springboot + vue + 协同过滤算法实现的旅游推荐系统

项目描述 本系统包含管理员和用户两个角色。 管理员角色&#xff1a; 用户管理&#xff1a;管理系统中所有用户的信息&#xff0c;包括添加、删除和修改用户。 配置管理&#xff1a;管理系统配置参数&#xff0c;如上传图片的路径等。 权限管理&#xff1a;分配和管理不同角…

AI数据治理破局的战略重构

AI数据治理破局的战略重构 AI正在颠覆传统数据治理模式动态策略驱动的AI治理新模式构建AI时代的数据防护栏结语 人工智能正重塑商业世界&#xff0c;那些真正理解当代数据治理变革的企业将占据决定性优势。 旧日的数据治理手册已经无法应对AI时代的全新挑战&#xff0c;我们需要…

QT6安装与概念介绍

文章目录 前言installModulesQt Core元对象系统属性系统对象模型对象树和所有者信号 & 槽 前言 QT不是纯粹的C标准&#xff0c;它在此基础上引入MOC编译器&#xff0c;在调用C编译器之前会使用该编译器将非C的内容如 Q_OBJECT、signal:等进行处理。此外QT还引入了对象间通…