Semaphore 实战:数据库、缓存、网络连接优化全攻略,让你的 Java 项目飞起来!
嘿,哥们儿!我是老码农了,今天咱不聊那些虚头巴脑的理论,直接上干货!咱们聊聊怎么用 Java 里的 Semaphore
优化数据库连接、缓存和网络连接,让你的项目像装了火箭一样飞起来!
为什么要用 Semaphore?
首先,咱们得搞清楚为啥要用 Semaphore
。在多线程编程里,经常会遇到资源竞争的问题,比如数据库连接、缓存的访问、网络连接等等。如果多个线程同时去抢这些资源,很容易出现问题:
- 资源耗尽: 比如数据库连接池,如果连接数量被耗光了,后面的线程就得一直等着,甚至直接抛异常,这谁受得了?
- 性能下降: 大量线程争抢资源,会导致线程切换频繁,CPU 负载飙升,系统响应速度变慢,用户体验直接降到冰点。
- 系统崩溃: 如果资源访问没有控制好,可能导致死锁、饥饿等问题,最终导致系统崩溃。
Semaphore
就像一个“信号量”,可以限制同时访问资源的线程数量。它维护了一个许可证的集合。线程要访问资源,必须先获取许可证;使用完资源后,释放许可证。如果许可证不够了,线程就得乖乖等着,直到有许可证可用。
Semaphore 的基本用法
Semaphore
的用法非常简单,核心就三个方法:
Semaphore(int permits)
: 构造方法,参数permits
指定了许可证的数量,也就是允许同时访问资源的线程数量。acquire()
: 获取一个许可证。如果许可证不够,线程会阻塞,直到有许可证可用。release()
: 释放一个许可证。
下面是一个简单的例子:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static final int MAX_PERMITS = 3; // 允许同时访问资源的线程数量
private static final Semaphore semaphore = new Semaphore(MAX_PERMITS);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + " 获取了资源,开始工作...");
Thread.sleep(2000); // 模拟资源使用
System.out.println(Thread.currentThread().getName() + " 工作完成,释放资源...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
}
}, "Thread-" + i).start();
}
}
}
在这个例子里,Semaphore
限制了同时访问资源的线程数量为 3。当有线程尝试获取许可证时,如果许可证不够,它就会被阻塞,直到有其他线程释放许可证。运行这段代码,你会看到只有 3 个线程能同时执行 System.out.println
语句。
数据库连接优化实战
数据库连接是项目中最常见的资源,连接数量的限制直接影响到系统的并发处理能力。如果数据库连接池设置不合理,很容易成为系统的瓶颈。
1. 数据库连接池的常见问题
- 连接泄漏: 线程获取了连接,但是忘记释放了,导致连接池中的连接越来越少,最终耗尽。
- 连接超时: 数据库连接长时间没有活动,被数据库服务器关闭,导致连接失效。
- 连接数不足: 并发量上来之后,连接池中的连接数量不够用,导致线程阻塞。
2. 使用 Semaphore 优化数据库连接池
咱们可以结合 Semaphore
和数据库连接池,控制同时使用的数据库连接数量。这里我以 HikariCP
为例,这是一个高性能的数据库连接池。
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.Semaphore;
public class DatabaseConnectionExample {
private static final int MAX_CONNECTIONS = 10; // 最大连接数
private static final Semaphore connectionSemaphore = new Semaphore(MAX_CONNECTIONS);
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/your_database"); // 替换为你的数据库 URL
config.setUsername("your_username"); // 替换为你的数据库用户名
config.setPassword("your_password"); // 替换为你的数据库密码
config.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 替换为你的数据库驱动类名
config.setMaximumPoolSize(MAX_CONNECTIONS); // 设置连接池最大连接数
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws SQLException, InterruptedException {
connectionSemaphore.acquire(); // 获取许可证,控制并发连接数
try {
return dataSource.getConnection();
} catch (SQLException e) {
connectionSemaphore.release(); // 如果获取连接失败,释放许可证
throw e;
}
}
public static void releaseConnection(Connection connection) {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
connectionSemaphore.release(); // 释放许可证
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
Connection connection = null;
try {
connection = getConnection();
System.out.println(Thread.currentThread().getName() + " 获取了数据库连接");
// 模拟数据库操作
Thread.sleep(1000); // 模拟数据库操作时间
} catch (SQLException | InterruptedException e) {
e.printStackTrace();
} finally {
releaseConnection(connection);
System.out.println(Thread.currentThread().getName() + " 释放了数据库连接");
}
}, "Thread-" + i).start();
}
}
}
代码解释:
- 初始化连接池: 使用
HikariConfig
初始化连接池,设置数据库的连接信息,包括 URL、用户名、密码、驱动类名,以及连接池的最大连接数MAX_CONNECTIONS
。 - 创建 Semaphore: 创建一个
Semaphore
对象connectionSemaphore
,许可证数量设置为MAX_CONNECTIONS
,与连接池的最大连接数保持一致。 - 获取连接:
getConnection()
方法首先调用connectionSemaphore.acquire()
,获取一个许可证。如果许可证不够,线程会被阻塞。然后,从连接池中获取一个数据库连接。 - 释放连接:
releaseConnection()
方法首先关闭数据库连接,然后调用connectionSemaphore.release()
,释放一个许可证。特别注意,为了防止异常情况,releaseConnection
方法应该放在finally
块中,确保连接一定会被释放。 - 主函数模拟并发访问: 在
main
方法中,创建 20 个线程并发地获取和释放数据库连接,模拟高并发场景。
优化建议:
- 设置合理的连接池大小: 根据实际的业务需求和数据库服务器的负载能力,设置合适的
MAX_CONNECTIONS
值。过小的连接池会导致线程阻塞,过大的连接池会占用过多的资源。 - 监控数据库连接池: 使用连接池的监控工具,比如
HikariCP
提供的监控指标,监控连接池的连接使用情况、连接等待时间等,及时发现问题。 - 连接超时设置: 设置数据库连接的超时时间,避免长时间的阻塞。如果连接超时,应该及时释放连接。
- 使用 try-with-resources: 在 Java 7 及以上版本,可以使用
try-with-resources
语句,自动关闭资源,简化代码,减少连接泄漏的风险。
缓存优化实战
缓存是提高系统性能的利器,它可以减少对数据库的访问,提高响应速度。但是,缓存也可能成为系统的瓶颈,比如缓存击穿、缓存雪崩等问题。
1. 缓存的常见问题
- 缓存击穿: 某个热点数据失效了,大量的请求直接打到数据库上,导致数据库负载过高。
- 缓存雪崩: 大量的缓存数据同时失效,导致大量的请求涌入数据库,系统崩溃。
- 缓存穿透: 请求的数据在缓存和数据库中都不存在,每次请求都会打到数据库上。
2. 使用 Semaphore 优化缓存访问
咱们可以利用 Semaphore
控制同时访问缓存的线程数量,避免缓存击穿和缓存雪崩。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
public class CacheExample {
private static final int MAX_CONCURRENT_READS = 10; // 允许同时读取缓存的线程数量
private static final Semaphore readSemaphore = new Semaphore(MAX_CONCURRENT_READS);
private static final Map<String, String> cache = new HashMap<>();
// 模拟从数据库获取数据
private static String getDataFromDatabase(String key) {
System.out.println("从数据库获取数据,key: " + key);
// 模拟耗时操作
try {
Thread.sleep(100); // 模拟数据库查询时间
} catch (InterruptedException e) {
e.printStackTrace();
}
return "数据库数据: " + key;
}
public static String getData(String key) {
String value = cache.get(key);
if (value != null) {
System.out.println("从缓存获取数据,key: " + key);
return value;
}
try {
readSemaphore.acquire(); // 获取许可证
// 再次检查缓存,防止其他线程已经更新了缓存
value = cache.get(key);
if (value != null) {
System.out.println("从缓存获取数据(并发更新后),key: " + key);
return value;
}
// 从数据库获取数据
value = getDataFromDatabase(key);
// 更新缓存
cache.put(key, value);
System.out.println("从数据库获取数据,更新缓存,key: " + key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readSemaphore.release(); // 释放许可证
}
return value;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
final String key = "data-" + (i % 5); // 模拟热点数据
new Thread(() -> {
String value = getData(key);
System.out.println(Thread.currentThread().getName() + ": " + value);
}, "Thread-" + i).start();
}
}
}
代码解释:
- 创建 Semaphore: 创建一个
Semaphore
对象readSemaphore
,许可证数量设置为MAX_CONCURRENT_READS
,限制同时读取缓存的线程数量。 getData
方法:- 首先从缓存中获取数据,如果存在,直接返回。
- 如果缓存中不存在,调用
readSemaphore.acquire()
获取许可证。如果许可证不够,线程会被阻塞。 - 双重检查锁: 在获取到许可证后,再次检查缓存。这是为了防止多个线程同时从数据库获取数据,导致缓存被多次更新。如果其他线程已经更新了缓存,则直接从缓存中获取数据。
- 从数据库获取数据,更新缓存。
- 调用
readSemaphore.release()
释放许可证。
main
方法模拟并发访问: 创建 20 个线程并发地访问缓存,模拟高并发场景,其中key
模拟了热点数据。
优化建议:
- 设置合理的并发读取数量: 根据缓存服务器的性能和数据库的负载能力,设置合适的
MAX_CONCURRENT_READS
值。 - 使用分布式锁: 对于更新缓存的操作,可以使用分布式锁,保证只有一个线程可以更新缓存,避免并发更新带来的问题。
- 缓存预热: 在系统启动时,预先加载一些热点数据到缓存中,减少缓存击穿的概率。
- 缓存失效策略: 选择合适的缓存失效策略,比如 LRU、LFU 等,避免缓存雪崩。
- 使用缓存框架: 使用成熟的缓存框架,比如 Redis、Memcached 等,可以简化缓存管理,提高性能。
网络连接优化实战
网络连接也是项目中常见的资源,比如 HTTP 连接、Socket 连接等等。如果网络连接管理不当,会导致连接数过多,系统性能下降。
1. 网络连接的常见问题
- 连接数过多: 客户端和服务端建立的连接数过多,导致资源耗尽。
- 连接超时: 网络不稳定或者服务端处理时间过长,导致连接超时。
- 连接泄漏: 连接建立后,没有正确关闭,导致连接泄漏。
2. 使用 Semaphore 优化网络连接
我们可以使用 Semaphore
限制同时建立的网络连接数量,避免连接数过多。
import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.Semaphore;
public class NetworkConnectionExample {
private static final int MAX_CONNECTIONS = 5; // 允许同时建立的网络连接数量
private static final Semaphore connectionSemaphore = new Semaphore(MAX_CONNECTIONS);
private static final String SERVER_ADDRESS = "localhost";
private static final int SERVER_PORT = 8080;
public static void processRequest(int requestId) {
Socket socket = null;
try {
connectionSemaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + " 获取了连接,请求 ID: " + requestId);
socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
// 模拟网络请求
Thread.sleep(1000); // 模拟网络请求时间
System.out.println(Thread.currentThread().getName() + " 请求 ID: " + requestId + " 完成");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
connectionSemaphore.release(); // 释放许可证
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
final int requestId = i;
new Thread(() -> processRequest(requestId), "Thread-" + i).start();
}
}
}
代码解释:
- 创建 Semaphore: 创建一个
Semaphore
对象connectionSemaphore
,许可证数量设置为MAX_CONNECTIONS
,限制同时建立的网络连接数量。 processRequest
方法:- 调用
connectionSemaphore.acquire()
获取许可证,如果许可证不够,线程会被阻塞。 - 创建
Socket
连接到服务端。 - 模拟网络请求。
- 关闭
Socket
连接。 - 调用
connectionSemaphore.release()
释放许可证。
- 调用
main
方法模拟并发请求: 创建 10 个线程并发地发送网络请求,模拟高并发场景。
优化建议:
- 连接池技术: 对于 HTTP 连接,可以使用连接池技术,比如
Apache HttpClient
或者OkHttp
,复用连接,减少连接的开销。 - 连接超时设置: 设置网络连接的超时时间,避免长时间的阻塞。
- 心跳机制: 对于长连接,可以使用心跳机制,检测连接是否存活,及时关闭失效的连接。
- 异步处理: 对于耗时的网络请求,可以使用异步处理,提高系统的响应速度。
Semaphore 的高级用法
除了基本的 acquire()
和 release()
方法,Semaphore
还有一些高级用法,可以更灵活地控制资源访问。
1. tryAcquire()
方法
tryAcquire()
方法尝试获取一个许可证,如果成功,返回 true
;如果失败,立即返回 false
,不会阻塞线程。这可以用于避免线程长时间阻塞。
if (semaphore.tryAcquire()) {
try {
// 访问资源
} finally {
semaphore.release();
}
} else {
// 无法获取许可证,处理其他逻辑
System.out.println(Thread.currentThread().getName() + " 无法获取许可证");
}
2. tryAcquire(long timeout, TimeUnit unit)
方法
tryAcquire(long timeout, TimeUnit unit)
方法尝试获取一个许可证,如果在指定的时间内获取到许可证,返回 true
;如果超时,返回 false
,线程不会被阻塞太久。
if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
try {
// 访问资源
} finally {
semaphore.release();
}
} else {
// 无法获取许可证,处理其他逻辑
System.out.println(Thread.currentThread().getName() + " 获取许可证超时");
}
3. drainPermits()
方法
drainPermits()
方法一次性获取所有可用的许可证,并返回获取的许可证数量。这个方法可以用于快速清空 Semaphore
中的所有许可证。
int permits = semaphore.drainPermits();
System.out.println("获取了 " + permits + " 个许可证");
总结
Semaphore
是一个非常强大的并发工具,可以帮助我们优化数据库连接、缓存和网络连接等资源的访问,提高系统的响应速度和稳定性。在使用 Semaphore
时,需要注意以下几点:
- 设置合适的许可证数量: 根据实际的业务需求和资源负载能力,设置合适的许可证数量,避免资源耗尽或者线程阻塞。
- 正确释放许可证: 确保在资源使用完毕后,及时释放许可证,避免资源泄漏。
- 处理异常情况: 在获取许可证和释放许可证的过程中,需要处理异常情况,比如
InterruptedException
,确保程序的健壮性。 - 结合其他并发工具:
Semaphore
可以与其他并发工具,比如Lock
、BlockingQueue
等,结合使用,实现更复杂的并发控制逻辑。
希望这篇文章能帮助你更好地理解和使用 Semaphore
,让你的 Java 项目更加高效、稳定!
加油,老铁!