HOOOS

Semaphore 实战:数据库、缓存、网络连接优化全攻略,让你的 Java 项目飞起来!

0 92 老码农的救赎 JavaSemaphore并发编程
Apple

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();
        }
    }
}

代码解释:

  1. 初始化连接池: 使用 HikariConfig 初始化连接池,设置数据库的连接信息,包括 URL、用户名、密码、驱动类名,以及连接池的最大连接数 MAX_CONNECTIONS
  2. 创建 Semaphore: 创建一个 Semaphore 对象 connectionSemaphore,许可证数量设置为 MAX_CONNECTIONS,与连接池的最大连接数保持一致。
  3. 获取连接: getConnection() 方法首先调用 connectionSemaphore.acquire(),获取一个许可证。如果许可证不够,线程会被阻塞。然后,从连接池中获取一个数据库连接。
  4. 释放连接: releaseConnection() 方法首先关闭数据库连接,然后调用 connectionSemaphore.release(),释放一个许可证。特别注意,为了防止异常情况,releaseConnection 方法应该放在 finally 块中,确保连接一定会被释放。
  5. 主函数模拟并发访问: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();
        }
    }
}

代码解释:

  1. 创建 Semaphore: 创建一个 Semaphore 对象 readSemaphore,许可证数量设置为 MAX_CONCURRENT_READS,限制同时读取缓存的线程数量。
  2. getData 方法:
    • 首先从缓存中获取数据,如果存在,直接返回。
    • 如果缓存中不存在,调用 readSemaphore.acquire() 获取许可证。如果许可证不够,线程会被阻塞。
    • 双重检查锁: 在获取到许可证后,再次检查缓存。这是为了防止多个线程同时从数据库获取数据,导致缓存被多次更新。如果其他线程已经更新了缓存,则直接从缓存中获取数据。
    • 从数据库获取数据,更新缓存。
    • 调用 readSemaphore.release() 释放许可证。
  3. 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();
        }
    }
}

代码解释:

  1. 创建 Semaphore: 创建一个 Semaphore 对象 connectionSemaphore,许可证数量设置为 MAX_CONNECTIONS,限制同时建立的网络连接数量。
  2. processRequest 方法:
    • 调用 connectionSemaphore.acquire() 获取许可证,如果许可证不够,线程会被阻塞。
    • 创建 Socket 连接到服务端。
    • 模拟网络请求。
    • 关闭 Socket 连接。
    • 调用 connectionSemaphore.release() 释放许可证。
  3. 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 可以与其他并发工具,比如 LockBlockingQueue 等,结合使用,实现更复杂的并发控制逻辑。

希望这篇文章能帮助你更好地理解和使用 Semaphore,让你的 Java 项目更加高效、稳定!

加油,老铁!

点评评价

captcha
健康