进阶篇:同步阻塞队列之LinkedBlockingQueue(十一)

本文介绍Java中的LinkedBlockingQueue,一种遵循先进先出原则的阻塞队列,并通过示例展示了如何利用该队列实现简单的生产者消费者模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDK为我们提供了多个阻塞队列的实现,什么是阻塞队列呢? 我们都知道队列就是一组数据的集合,而阻塞队列的意思是,当你往队列中取数据时,如果没有数据,你将被阻塞,一直等到拿到数据为止;

今天我们就来看一下比较常用的遵循先进先出的阻塞队列LinkedBlockingQueue;

//同步阻塞队列
	//可以看到,当调用take()方法去拿数据时,如果里面没有数据,将造成阻塞
	public static void blockingQueue(){
		final BlockingQueue<String> queue = new LinkedBlockingQueue<>();
		ExecutorService exec = Executors.newCachedThreadPool();
		//一个线程不断的往队列中取东西
		exec.execute(new Runnable() {
			public void run() {
				while( !Thread.currentThread().isInterrupted() ){
					try {
						String value = queue.take();
						System.out.println("线程"+Thread.currentThread()+"拿到数据:"+value);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		//一个线程不断的往队列中放东西
		exec.execute(new Runnable() {
			public void run() {
				while( !Thread.currentThread().isInterrupted() ){
					try {
						TimeUnit.MILLISECONDS.sleep(1000);
						int number = new Random().nextInt(1000);
						System.out.println("写入数据:"+number);
						queue.put(number+"");
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
	}


简单吧!一个线程写数据,一个线程读数据,当读数据的线程调用queue.take()往队列中拿数据时,如果队列中没有数据,它就一直阻塞,直到写数据的线程通过queue.put()方法写入数据为止! 是不是比我们自己写wait()和notify()要简单好用的多呢?!


对比上一篇文章的生产者消费者的实现,如果我们采用阻塞队列来实现的话,会变成什么样子呢?我们来看一下吧!

//餐厅
class BlockingRestaurant{
	public BlockingQueue<String> queue = new LinkedBlockingQueue<>();
}

//食物消费者(顾客)
class BlockingConsumer{
	//在这家餐厅就餐
	private BlockingRestaurant res = null;
	public BlockingConsumer( BlockingRestaurant res ) {
		this.res = res;
	}
	//吃食物方法
	public void eat() throws InterruptedException{
		while( !Thread.currentThread().isInterrupted() ){
			if( res.queue.take()!=null ){
				System.out.println("开始吃食物...");
				TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));//吃食物时间
				System.out.println("食物吃完了!...");
			}
		}
	}
}

//食物提供者(厨师)
class BlockingProvider{
	//在这家餐厅工作
	private BlockingRestaurant res =  null;
	public BlockingProvider( BlockingRestaurant res ) {
		this.res = res;
	}
	
	public void fry() throws InterruptedException{//炒菜
		while( !Thread.currentThread().isInterrupted() ){
			System.out.println("开始做食物...");
			TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
			res.queue.put("一份食物");
			System.out.println("食物做完了!...");
		}
	}
}
//经典生产者与消费者演示(采用队列)
	public static void producerByQueue(){
		ExecutorService exec = Executors.newCachedThreadPool();
		BlockingRestaurant res = new BlockingRestaurant();
		final BlockingConsumer consumer = new BlockingConsumer(res);
		final BlockingProvider provider = new BlockingProvider(res);
		
		exec.execute(new Runnable() {
			public void run() {
				try {
					consumer.eat();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		
		exec.execute(new Runnable() {
			public void run() {
				try {
					provider.fry();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
	}

简单多了吧!  Ok,这就是LinkedBlockingQueue的使用方法,后面我们会再介绍优先级队列,延时队列等等多种队列,它们将实现更有趣的功能!


<think>好的,用户问的是如何设置队列的最大长度。我需要先回顾之前的对话,看看用户之前的需求是什么。之前用户问的是设计一个实时统计过去5分钟内访问次数最多的1000个IP的Java程序。现在这个问题是关于队列最大长度的设置,可能是在优化内存使用,防止队列无限增长导致内存溢出。 首先,用户可能在之前的方案中看到使用了LinkedBlockingQueue来存储时间戳,但可能没有限制队列的大小,所以在高流量下,队列可能会占用太多内存。用户现在想知道如何设置队列的最大长度,可能是为了避免这种情况。 接下来,我需要考虑不同的队列实现,比如LinkedBlockingQueue默认是无界的,如果用户需要限制长度,可能需要使用有界队列。但Java中的LinkedBlockingQueue可以通过构造函数指定容量,这样当队列满时,添加操作会阻塞或返回错误。不过,在高并发环境下,阻塞可能影响性能,或者用户可能需要更灵活的处理策略,比如丢弃旧数据或新数据。 然后,用户的需求可能是在达到最大长度时,自动移除旧数据,保持队列只包含最新的记录。比如,当新数据加入时,如果队列已满,就移除最旧的数据,这样队列长度始终不超过限制,同时保证最新的数据在窗口内。这可能更符合滑动窗口的需求,因为旧数据即使没有过期,也可能因为超过队列长度而被移除,但需要权衡时间窗口和队列长度的限制。 另外,在之前的代码中,用户使用了LinkedBlockingQueue,但ConcurrentHashMap中的队列可能需要线程安全的处理。所以需要确保在修改队列时,线程安全,并且处理队列满的情况,比如使用offer方法而不是add,避免抛出异常,或者使用poll来移除旧元素。 可能的解决方案包括:使用有界队列,并在添加新元素时检查队列大小,如果超过限制,则移除队首元素。或者使用自定义的队列实现,在添加时自动维护最大长度。例如,在每次添加新时间戳时,如果队列大小已达到最大长度,就先移除一个旧的时间戳,再添加新的,这样可以保持队列长度固定,同时可能牺牲一些数据的准确性,但保证内存使用可控。 还需要考虑这种设置对计数的影响,因为当队列满时移除旧元素,可能需要同时减少对应的计数器。例如,在添加新时间戳时,如果队列已满,移除的旧时间戳对应的计数应该递减,并且如果计数减到零,可能需要从ConcurrentHashMap中移除该IP条目,避免内存泄漏。 此外,用户可能关心实现的具体代码,比如如何在Java中实现这样的队列行为。可能需要使用Deque或者自定义的队列结构,在添加元素时进行大小检查,并处理旧元素的移除和计数的更新。 总结来说,用户的需求是如何在Java中设置队列的最大长度,特别是在高并发环境下,保证队列不会无限增长,同时正确处理计数器的更新。需要提供具体的代码示例,展示如何在添加元素时维护队列的最大长度,并处理相关的计数逻辑。</think>以下是针对队列最大长度设置的3种实现方案及详细说明: ### 一、基础方案:固定长度队列(自动淘汰旧数据) ```java // 在初始化时定义最大长度 private static final int MAX_QUEUE_SIZE = 10000; // 修改时间队列更新逻辑 ipTimeQueue.compute(ip, (k, queue) -> { if (queue == null) { queue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE); } // 当队列满时,先移除最旧数据 while (queue.size() >= MAX_QUEUE_SIZE) { Long expired = queue.poll(); // 移除最旧时间戳 ipCountMap.get(ip).decrementAndGet(); // 同步更新计数器 } queue.offer(timestamp); return queue; }); ``` ### 二、进阶方案:环形缓冲区(内存优化) ```java // 自定义环形队列实现 class CircularQueue { private final long[] buffer; private int head = 0; private int tail = 0; private final int capacity; public CircularQueue(int capacity) { this.capacity = capacity; this.buffer = new long[capacity]; } public synchronized void add(long value) { buffer[tail % capacity] = value; if (size() == capacity) { head++; // 队列满时移动头指针 } tail++; } public synchronized int size() { return tail - head; } } // 使用方式 ipTimeQueue.computeIfAbsent(ip, k -> new CircularQueue(10000)).add(timestamp); ``` ### 三、生产级方案:Guava EvictingQueue(推荐) ```java // 添加Maven依赖 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency> // 实现代码 import com.google.common.collect.EvictingQueue; // 初始化时定义队列 private static final int MAX_QUEUE_SIZE = 10000; ipTimeQueue.compute(ip, (k, queue) -> { if (queue == null) { queue = EvictingQueue.create(MAX_QUEUE_SIZE); } // 自动淘汰旧元素 if (queue.size() >= MAX_QUEUE_SIZE) { Long expired = queue.peek(); // 获取将被淘汰的元素 ipCountMap.get(ip).decrementAndGet(); } queue.add(timestamp); return queue; }); ``` ### 四、各方案对比 | 特性 | 基础方案 | 环形缓冲区 | Guava方案 | |--------------------|------------------|------------------|------------------| | 时间复杂度 | O(1)~O(n) | O(1) | O(1) | | 线程安全性 | 需额外同步 | 内置同步 | 需外部同步 | | 内存效率 | 中等 | 高 | 中等 | | 淘汰策略 | FIFO | FIFO | FIFO | | 第三方依赖 | 无 | 无 | 需要Guava | | 适用场景 | 低并发场景 | 高频写入场景 | 快速实现场景 | ### 五、关键注意事项 1. **线程安全**:所有队列操作需要同步控制 ```java // 使用ConcurrentHashMap的compute方法保证原子性 ipTimeQueue.compute(ip, (k, queue) -> { // 操作代码 return queue; }); ``` 2. **计数一致性**:淘汰旧数据同步更新计数器 ```java // 在元素被淘汰时执行 if (queue.size() >= MAX_QUEUE_SIZE) { Long expired = queue.poll(); AtomicInteger counter = ipCountMap.get(ip); if (counter.decrementAndGet() == 0) { ipCountMap.remove(ip); // 清理空计数器 } } ``` 3. **性能监控**:建议添加指标统计 ```java // 记录队列淘汰次数 AtomicLong evictCount = new AtomicLong(); // 在淘汰逻辑中添加 evictCount.incrementAndGet(); ``` 4. **动态调整**:运行时修改队列长度 ```java // 使用代理模式实现动态调整 class ResizableQueue { private volatile int maxSize; private Queue<Long> queue = new LinkedList<>(); public synchronized void setMaxSize(int newSize) { this.maxSize = newSize; while (queue.size() > newSize) { queue.poll(); // 更新计数器... } } } ``` ### 六、生产环境建议 1. **容量计算公式**: $$ 最大队列长度 = \frac{预期最大QPS \times 窗口时间(秒)}{去重系数} $$ 示例:预期5000 QPS,5分钟窗口,假设去重系数2 $$ \frac{5000 \times 300}{2} = 750,000 $$ 2. **监控指标**: - 队列淘汰率(evict_rate) - 平均队列长度(queue_size_avg) - 最大队列长度(queue_size_max) 3. **异常处理**: ```java try { queue.add(timestamp); } catch (IllegalStateException e) { // Guava队列满时会抛出异常 metrics.counter("queue_full_errors").increment(); } ``` 这些方案可根据实际业务场景组合使用,建议优先考虑Guava方案+动态调整能力,在保证功能的同时降低实现复杂度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值