You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中list.isEmpty()/size()在if循环失效及多线程UDP接收问题求助

解决UDP接收端多线程集合操作的并发问题

看起来你踩了Java多线程集合操作的经典坑——非线程安全集合的并发访问问题,不管是之前用Queue遇到的Iterator异常,还是现在用List出现的isEmpty()/size()失效,本质都是同一个原因:普通的ArrayList/LinkedList(包括用它当Queue用)都不是为多线程场景设计的,缺乏同步机制和内存可见性保证。

问题根源拆解

  1. Queue的Iterator异常:如果之前用的是普通LinkedList作为Queue,当一个线程在迭代它的时候,另一个线程修改了集合内容,Java的fail-fast机制会直接抛出ConcurrentModificationException——这不是低级错误,是非线程安全集合的固有特性。
  2. List的isEmpty()/size()失效:普通List没有内存屏障,线程1添加元素后,线程2的CPU缓存可能没有同步主存的最新数据,导致明明List里有元素,isEmpty()却返回true,size()也显示为0;另外,即使你看到了size>0,在你取出元素的瞬间,可能已经被其他线程拿走了,出现竞态条件。

最优解决方案:改用线程安全的并发集合

Java的java.util.concurrent包专门提供了适合多线程生产-消费场景的集合,最推荐你用**ConcurrentLinkedQueue**——它是无锁实现的线程安全队列,完美适配你的UDP接收+后台处理的场景:

接收端示例代码

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.concurrent.ConcurrentLinkedQueue;

public class UDPStreamReceiver {
    // 线程安全的队列,用于缓存UDP接收的数据
    private final ConcurrentLinkedQueue<DatagramPacket> dataQueue = new ConcurrentLinkedQueue<>();
    private static final int BUFFER_SIZE = 1024;
    private static final int PORT = 8888;

    // 线程1:负责接收UDP数据并写入队列
    private class UDPReceiveThread extends Thread {
        @Override
        public void run() {
            try (DatagramSocket socket = new DatagramSocket(PORT)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                while (!Thread.currentThread().isInterrupted()) {
                    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                    socket.receive(packet);
                    // 将接收到的数据包存入队列,offer()是线程安全的
                    dataQueue.offer(packet);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 线程2:负责从队列取出数据并处理
    private class DataProcessThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                // poll()方法会原子性地取出队列头部元素,为空则返回null
                DatagramPacket packet = dataQueue.poll();
                if (packet != null) {
                    // 执行你的业务操作
                    processReceivedData(packet);
                } else {
                    // 队列为空时短暂休眠,避免CPU空转
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }

    // 自定义的数据处理逻辑
    private void processReceivedData(DatagramPacket packet) {
        String content = new String(packet.getData(), 0, packet.getLength());
        System.out.println("处理事件:" + content);
        // 这里可以添加你的业务操作,比如解析、存储等
    }

    public static void main(String[] args) {
        UDPStreamReceiver receiver = new UDPStreamReceiver();
        receiver.new UDPReceiveThread().start();
        receiver.new DataProcessThread().start();
    }
}

如果一定要用List怎么办?

如果你坚持要用List作为缓冲,必须用同步包装类或者手动加锁,并且要保证所有对List的操作(包括检查和修改)都在同一个同步块内:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.net.DatagramPacket;

public class SyncListReceiver {
    // 用Collections.synchronizedList包装普通List,得到线程安全的List
    private final List<DatagramPacket> syncDataList = Collections.synchronizedList(new ArrayList<>());

    // 线程1添加数据(add()是线程安全的,但检查+取出必须同步)
    private class ReceiveThread extends Thread {
        @Override
        public void run() {
            // UDP接收逻辑...
            syncDataList.add(packet);
        }
    }

    // 线程2处理数据
    private class ProcessThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                DatagramPacket packet = null;
                // 必须把isEmpty()和remove()放在同一个同步块里,避免竞态条件
                synchronized (syncDataList) {
                    if (!syncDataList.isEmpty()) {
                        packet = syncDataList.remove(0);
                    }
                }
                if (packet != null) {
                    processReceivedData(packet);
                } else {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    }

    // 处理逻辑...
    private void processReceivedData(DatagramPacket packet) {
        // 自定义业务操作
    }
}

不过这种方式的效率远不如ConcurrentLinkedQueue,因为每次操作都要加锁,适合低并发场景。

关键总结

  • 多线程生产-消费场景下,优先用java.util.concurrent包的线程安全集合,ConcurrentLinkedQueue是你的最佳选择;
  • 非线程安全集合的并发操作会导致各种诡异问题(可见性、竞态条件、fail-fast异常),千万别在多线程里直接用;
  • 如果你之前用Queue时遇到Iterator问题,换成ConcurrentLinkedQueue就解决了——它的Iterator是弱一致的,不会抛出ConcurrentModificationException。

内容的提问来源于stack exchange,提问作者Amarjit Dhillon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 11:47:36