Java中list.isEmpty()/size()在if循环失效及多线程UDP接收问题求助
解决UDP接收端多线程集合操作的并发问题
看起来你踩了Java多线程集合操作的经典坑——非线程安全集合的并发访问问题,不管是之前用Queue遇到的Iterator异常,还是现在用List出现的isEmpty()/size()失效,本质都是同一个原因:普通的ArrayList/LinkedList(包括用它当Queue用)都不是为多线程场景设计的,缺乏同步机制和内存可见性保证。
问题根源拆解
- Queue的Iterator异常:如果之前用的是普通
LinkedList作为Queue,当一个线程在迭代它的时候,另一个线程修改了集合内容,Java的fail-fast机制会直接抛出ConcurrentModificationException——这不是低级错误,是非线程安全集合的固有特性。 - 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
相关产品推荐
相关产品推荐

