MST

星途 面试题库

面试题:Java网络编程之多线程Socket应用

假设你需要开发一个支持多客户端连接的Java服务器程序,使用Socket进行通信,描述如何运用多线程技术来处理并发客户端请求,以确保高效和稳定的通信,并且说明可能会遇到哪些线程安全问题以及如何解决。
43.2万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

运用多线程技术处理并发客户端请求

  1. 服务器端主线程
    • 创建一个 ServerSocket 实例,绑定到指定端口,用于监听客户端连接请求。
    • 使用 while (true) 循环持续监听客户端连接。当有客户端连接时,调用 serverSocket.accept() 方法,该方法会阻塞直到有新的客户端连接进来,此时返回一个 Socket 实例代表与客户端的连接。
  2. 为每个客户端创建独立线程
    • 每当 accept() 方法返回一个新的 Socket 时,创建一个新的线程来处理该客户端的通信。通常的做法是创建一个实现了 Runnable 接口的类,在其 run() 方法中编写与客户端通信的逻辑,如读取客户端发送的数据、处理数据并向客户端发送响应。
    • 在主线程中,使用 new Thread(new ClientHandler(socket)).start() 来启动新线程,其中 ClientHandler 是实现了 Runnable 接口的类,构造函数接收 Socket 实例。
  3. 客户端处理线程逻辑
    • 在实现 Runnable 接口的类的 run() 方法中,通过 Socket 获取输入输出流,如 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))PrintWriter out = new PrintWriter(socket.getOutputStream(), true) 分别用于读取客户端输入和向客户端输出。
    • 使用循环读取客户端发送的数据,处理数据(例如业务逻辑计算等),然后将处理结果通过输出流返回给客户端。
    • 处理完通信后,关闭相关的流和 Socket 连接。

可能遇到的线程安全问题及解决方法

  1. 共享资源竞争
    • 问题描述:如果多个线程访问和修改共享资源(如全局变量、静态变量等),可能会导致数据不一致。例如,一个线程正在读取某个共享变量的值,同时另一个线程修改了该值,可能导致读取到不一致的数据。
    • 解决方法
      • 使用 synchronized 关键字:可以修饰方法或代码块。修饰方法时,该方法在同一时间只能被一个线程访问;修饰代码块时,只有获取到指定对象锁的线程才能执行该代码块。例如:
private static int sharedVariable;
public static synchronized void incrementSharedVariable() {
    sharedVariable++;
}
    - **使用 `Lock` 接口**:`java.util.concurrent.locks.Lock` 提供了比 `synchronized` 更灵活的锁机制。例如 `ReentrantLock`,使用时先创建锁实例,在需要同步的代码块前调用 `lock()` 方法获取锁,代码块结束后调用 `unlock()` 方法释放锁。
private static int sharedVariable;
private static final ReentrantLock lock = new ReentrantLock();
public static void incrementSharedVariable() {
    lock.lock();
    try {
        sharedVariable++;
    } finally {
        lock.unlock();
    }
}
  1. 死锁
    • 问题描述:两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行。例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1。
    • 解决方法
      • 避免嵌套锁:尽量减少锁的嵌套使用,如果无法避免,确保所有线程以相同顺序获取锁。
      • 设置锁超时:使用 tryLock() 方法尝试获取锁,并设置超时时间。如果在超时时间内未获取到锁,线程可以选择放弃或执行其他操作,避免无限等待。
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // 处理共享资源
    } catch (InterruptedException e) {
        // 处理中断异常
    } finally {
        lock.unlock();
    }
} else {
    // 未获取到锁的处理逻辑
}
  1. 线程局部变量误用
    • 问题描述ThreadLocal 用于为每个线程创建独立的变量副本。如果在使用 ThreadLocal 时逻辑错误,例如错误地共享了本应线程局部的变量,可能导致数据混乱。
    • 解决方法
      • 正确使用 ThreadLocal,确保每个线程独立地操作自己的变量副本。例如,在需要为每个线程维护独立的数据库连接时,可以这样使用 ThreadLocal
private static final ThreadLocal<Connection> connectionThreadLocal = ThreadLocal.withInitial(() -> {
    try {
        return DriverManager.getConnection(url, username, password);
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
});
    - 在使用完 `ThreadLocal` 变量后,及时调用 `remove()` 方法清理,避免内存泄漏。例如,在处理完数据库操作后:
connectionThreadLocal.remove();