如何使客户端-服务器套接字应用程序以同步方式访问数据并对其进行测试

How to make client-server socket applications access data in a synchronized way and test for it

我有这个使用 TCP 的简单服务器套接字应用程序,它允许多个客户端连接到服务器并从 ArrayList 存储和检索数据。 每次服务器接受客户端连接时,都会创建一个新线程来处理客户端 activity,并且由于所有客户端都必须与相同的数据结构交互,所以我决定创建一个静态 ArrayList,如下所示:

public class Repository {
    private static List<String> data;

    public static synchronized void insert(String value){
       if(data == null){
           data = new ArrayList<>();
       }
        data.add(value);
    }

    public static synchronized List<String> getData() {
        if(data == null){
            data = new ArrayList<>();
        }
        return data;
    }
}

因此,每次客户端插入一个值或读取列表时,他们只是从各自的线程中调用 Repository.insert(value)Repository.getData()

我的问题是:

  1. 使这些方法同步足以使操作线程安全?
  2. 这个静态列表是否存在任何架构或性能问题?我还可以在服务器 class(接受连接的服务器)中创建一个列表实例,并通过构造函数向线程发送一个引用,而不是使用静态的。这样好点了吗?
  3. 可以Collections.synchronizedList()为如此简单的任务增加任何价值吗?有必要吗?
  4. 在这种情况下如何测试线程安全?我试着创建多个客户端并让它们访问数据,一切似乎都正常,但我只是不相信......这是这个测试的一个简短片段:
            IntStream.range(0,10).forEach(i->{
            Client client = new Client();
            client.ConnectToServer();
            try {
                client.SendMessage("Hello from client "+i);
            } catch (IOException e) {
                e.printStackTrace();
            }});

            //assertions on the size of the array

提前致谢! :)

  1. 是的,参见
  2. 是的(请参阅下面的评论)是的。使用一个对象(如果你愿意,可以使用单例)优于静态方法(后者通常更难维护)。
  3. 不是必需的,但首选:它可以避免您犯错误。另外,而不是:

private static List<String> data;

你可以使用

private static final List<String> data = Collections.synchronizedList(new ArrayList<>());

这有三个好处:final 确保所有线程都能看到这个值(线程安全),您可以删除检查空值的代码(这很容易出错)并且您不会不再需要在您的代码中使用 synchronized,因为列表本身现在已同步。

  1. 是的,有一些方法可以改进这一点,使您更有可能发现错误,但是当涉及到多线程时,您永远无法完全确定。

关于“架构或性能问题”:列表的每次读取都必须同步,因此当多个客户端想要读取列表时,它们都将等待一个锁来读取整个列表。由于您只是在末尾插入并读取列表,因此您可以使用 ConcurrentLinkedQueue。这种“并发”类型(即不需要使用 synchronized - 队列是线程安全的)在读取整个列表时不会锁定,多个线程可以同时读取数据。此外,您应该隐藏您可以执行的实现细节,例如,使用 Iterator:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

    private final Queue<String> dataq = new ConcurrentLinkedQueue<>();
    
    public Iterator<String> getData() {
        return dataq.iterator();
    }

关于“测试线程安全”:关注需要线程安全的代码。使用客户端连接到服务器来测试 Repository 代码是否是线程安全的是低效的:大部分测试只会等待 I/O 而不是同时实际使用 Repository时间。只为 Repository class 编写专用(单元)测试。请记住,您的操作系统确定线程何时启动和 运行(即您的线程启动代码和 运行 只是对操作系统的提示),因此您需要测试 运行 一段时间(即 30 秒)并提供一些输出(日志记录)以确保线程同时 运行ning。到控制台的输出只应在测试结束时显示:在 Java 到控制台的输出 (System.out) 是同步的,这反过来可以使线程一个接一个地工作(即不同时)在 class 测试中)。 最后,您可以使用 java.util.concurrent.CountDownLatch 改进测试,让所有线程在并发执行下一个语句之前同步(这提高了找到竞争条件的机会)。对于这个已经很长的答案来说,解释起来有点多,所以我会给你一个(公认的复杂)example(关注如何使用 tableReady 变量)。