你好,我是爱琢磨的程序猿老李。今天咱们聊聊 Java 并发工具类 Semaphore(信号量)。很多开发者觉得 Semaphore 不就是控制并发线程数嘛,有啥难的?但真要用好它,在实际项目中发挥它的威力,可没那么简单。老李我就结合几个真实的项目案例,给你好好说道说道,Semaphore 到底怎么用才能真正提升系统的性能和稳定性。
Semaphore 是什么?
咱们先来简单回顾一下 Semaphore 的基本概念。Semaphore,中文叫信号量,顾名思义,它就像一个交通信号灯,控制着车辆(线程)的通行。它维护了一个许可集(permits),线程想要访问共享资源,就必须先从 Semaphore 那里获取一个许可。如果许可数量为 0,线程就得等着,直到有其他线程释放许可。
Semaphore 的基本用法非常简单,主要就两个操作:
acquire()
:获取一个许可。如果当前没有可用许可,线程就会阻塞,直到有可用许可为止。release()
:释放一个许可,将其归还给 Semaphore。
来,看段代码,感受一下:
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
private static final int MAX_CONNECTIONS = 5;
private static final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS, true); // 允许5个线程同时访问
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 正在访问数据库...");
Thread.sleep(2000); // 模拟数据库访问
System.out.println(Thread.currentThread().getName() + " 访问数据库完毕.");
semaphore.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
这段代码模拟了多个线程同时访问数据库的场景。我们创建了一个 Semaphore,初始许可数量为 5,表示最多允许 5 个线程同时访问数据库。每个线程在访问数据库前,先调用 acquire()
方法获取许可,访问完成后,再调用 release()
方法释放许可。这样,就能保证同时访问数据库的线程数不会超过 5 个,避免了数据库连接过多导致的性能问题。
注意到 new Semaphore(MAX_CONNECTIONS, true)
中的 true
了吗?这个参数表示 Semaphore 是否采用公平策略。如果设置为 true
,则 Semaphore 会按照线程请求的顺序来分配许可,也就是先到先得。如果设置为 false
,则 Semaphore 不保证分配许可的顺序,哪个线程抢到了就是谁的。一般情况下,为了避免线程饥饿,建议使用公平策略。
真实案例:Semaphore 优化数据库连接
光说不练假把式,咱们来看一个真实的案例。老李我之前参与过一个电商系统的开发,这个系统在高峰期经常出现数据库连接超时的问题。经过排查,发现是数据库连接池配置不合理,导致大量线程在等待数据库连接,最终超时。
怎么解决这个问题呢?我们首先想到的就是优化数据库连接池的配置,比如增大最大连接数、调整连接超时时间等等。但是,这些方法都有局限性。如果最大连接数设置得太大,可能会导致数据库负载过高;如果连接超时时间设置得太长,又可能会导致线程长时间阻塞,影响系统的响应速度。
这时候,Semaphore 就派上用场了。我们可以用 Semaphore 来控制同时访问数据库的线程数,将这个数量限制在一个合理的范围内,既能保证数据库的负载不会过高,又能避免线程长时间阻塞。具体怎么做呢?
- 改造数据库连接池:
我们自定义了一个数据库连接池,在连接池内部维护了一个 Semaphore。连接池初始化的时候,Semaphore 的许可数量等于连接池的最大连接数。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
public class MyConnectionPool {
private final int poolSize;
private final LinkedList<Connection> connections = new LinkedList<>();
private final Semaphore semaphore;
public MyConnectionPool(int poolSize, String url, String user, String password) throws SQLException {
this.poolSize = poolSize;
this.semaphore = new Semaphore(poolSize, true);
for (int i = 0; i < poolSize; i++) {
Connection connection = DriverManager.getConnection(url, user, password);
connections.add(connection);
}
}
public Connection getConnection() throws InterruptedException, SQLException {
semaphore.acquire(); // 获取许可
synchronized (connections) {
if (!connections.isEmpty()) {
return connections.removeFirst();
}
}
return null; //一般不会走到这里,除非连接池配置有问题
}
public void releaseConnection(Connection connection) {
if (connection != null) {
synchronized (connections) {
connections.addLast(connection);
}
semaphore.release(); // 释放许可
}
}
//关闭连接池中所有连接
public void close(){
synchronized (connections){
for(Connection conn : connections){
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
}
- 获取和释放连接:
当线程需要访问数据库时,先从连接池中获取连接。获取连接的时候,先调用 Semaphore 的 acquire()
方法获取许可。如果获取到了许可,就从连接池中取出一个连接;如果获取不到许可,线程就会阻塞,直到有其他线程释放连接。
当线程使用完连接后,将连接归还给连接池。归还连接的时候,调用 Semaphore 的 release()
方法释放许可。这样,其他等待连接的线程就有机会获取到许可,从而获取到连接。
通过这种方式,我们就用 Semaphore 对数据库连接进行了精细化的控制。无论并发量有多大,同时访问数据库的线程数都不会超过连接池的最大连接数,有效地避免了数据库连接超时的问题,提高了系统的稳定性和响应速度。
真实案例:Semaphore 优化缓存访问
再来看一个案例。老李我还参与过一个新闻资讯类 App 的开发,这个 App 有一个热门新闻排行榜的功能。为了提高访问速度,我们对热门新闻数据做了缓存。但是,在高并发的情况下,缓存的命中率反而下降了,这是为什么呢?
原来,当大量用户同时访问热门新闻排行榜时,如果缓存中没有对应的数据,就会触发缓存的加载操作。多个线程同时去加载缓存,会导致对数据库的重复查询,增加了数据库的压力,反而降低了缓存的命中率。
怎么解决这个问题呢?我们可以用 Semaphore 来控制同时加载缓存的线程数。具体来说,我们可以创建一个 Semaphore,初始许可数量为 1,表示只允许一个线程加载缓存。当线程需要访问缓存时,先尝试获取许可。如果获取到了许可,就说明当前没有其他线程在加载缓存,那么这个线程就可以去加载缓存,并在加载完成后释放许可。如果获取不到许可,就说明当前已经有其他线程在加载缓存了,那么这个线程就直接从缓存中获取数据,或者等待一段时间后重试。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
public class CacheManager {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private final Semaphore semaphore = new Semaphore(1, true); // 只允许一个线程加载缓存
public Object get(String key) throws InterruptedException {
Object value = cache.get(key);
if (value == null) {
if (semaphore.tryAcquire()) { // 尝试获取许可
try {
// 模拟从数据库加载数据
System.out.println(Thread.currentThread().getName() + " 正在加载缓存...");
Thread.sleep(1000);
value = "Data from DB"; // 假设这是从数据库加载的数据
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 加载缓存完毕.");
} finally {
semaphore.release(); // 释放许可
}
} else {
//其他线程正在加载,循环等待,直到有数据
while(value == null){
Thread.sleep(100); //稍作等待
value = cache.get(key);
}
}
}
return value;
}
public static void main(String[] args) {
CacheManager cacheManager = new CacheManager();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
Object data = cacheManager.get("hotNews");
System.out.println(Thread.currentThread().getName() + " 获取到数据:" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.setName("Thread-" + i);
thread.start();
}
}
}
这段代码中,我们使用 tryAcquire()
方法来尝试获取许可,而不是 acquire()
方法。tryAcquire()
方法会立即返回一个布尔值,表示是否获取到了许可。如果获取到了许可,就返回 true
;如果获取不到许可,就返回 false
,不会阻塞线程。
通过这种方式,我们就避免了多个线程同时加载缓存的问题,提高了缓存的命中率,降低了数据库的压力,提升了系统的性能。
真实案例:Semaphore 优化网络连接
除了数据库连接和缓存,Semaphore 还可以用来优化网络连接。比如,在爬虫程序中,我们需要控制同时发起的网络请求数量,避免对目标网站造成过大的压力,甚至被封 IP。我们可以用 Semaphore 来限制同时进行的网络连接数。
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.Semaphore;
public class WebCrawler {
private static final int MAX_CONNECTIONS = 10;
private static final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS, true);
private static final HttpClient httpClient = HttpClient.newHttpClient();
public static void main(String[] args) {
String[] urls = {"https://www.example.com", "https://www.google.com", "https://www.baidu.com" /*, ... 其他URL */};
for (String url : urls) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 开始爬取: " + url);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(Thread.currentThread().getName() + " 爬取完成: " + url);
} catch (IOException | InterruptedException e) {
System.err.println("爬取" + url +"失败:" + e.getMessage());
} finally {
semaphore.release();
}
});
thread.setName("Crawler-Thread-" + url.hashCode());
thread.start();
}
}
}
在这个例子中,我们创建了一个 Semaphore,初始许可数量为 10,表示最多允许 10 个线程同时进行网络请求。每个线程在发起网络请求前,先获取许可;请求完成后,释放许可。这样,就能保证同时进行的网络连接数不会超过 10 个,避免了对目标网站造成过大的压力。
Semaphore 使用注意事项
Semaphore 虽好,但使用的时候也要注意一些细节,避免踩坑:
正确设置许可数量:Semaphore 的许可数量要根据实际情况来设置。如果许可数量设置得太小,可能会导致线程频繁等待,降低系统的吞吐量;如果许可数量设置得太大,又可能起不到限流的作用。一般来说,许可数量可以设置为资源的数量,比如数据库连接池的最大连接数、线程池的核心线程数等。
正确地获取和释放许可:一定要确保在finally中释放,避免死锁。
注意 Semaphore 的公平性:Semaphore 有公平和非公平两种模式。公平模式下,Semaphore 会按照线程请求的顺序来分配许可,可以避免线程饥饿,但会降低一定的性能。非公平模式下,Semaphore 不保证分配许可的顺序,性能更高,但可能会导致某些线程一直获取不到许可。一般情况下,建议使用公平模式。
Semaphore不仅可以控制并发数,还可以实现更复杂的同步逻辑,比如生产者-消费者模型。通过精细地控制acquire和release的数量,可以实现很多同步需求。
总结
Semaphore 是 Java 并发编程中一个非常实用的工具,它可以帮助我们控制并发线程数,优化资源访问,提升系统的性能和稳定性。在实际项目中,我们可以用 Semaphore 来优化数据库连接、缓存访问、网络连接等等。老李我希望通过这篇文章,能让你对 Semaphore 有更深入的理解,在实际工作中更好地应用它。
记住,技术没有银弹,Semaphore 也不是万能的。在实际应用中,我们需要根据具体场景来选择合适的并发控制策略,才能达到最好的效果。希望你能多多实践,积累经验,成为一个真正的并发编程高手!
好了,今天就聊到这里。如果你还有其他关于 Semaphore 的问题,或者有其他并发编程方面的困惑,欢迎在评论区留言,老李我一定知无不言,言无不尽!