背景问题
我们都知道Java里的LinkedBlockingQueue,采用先进先出(FIFO)的方式存储元素,并且支持同时进行并发的读和写操作。内部使用ReentrantLock锁来保证多线程环境下的线程安全性。
LinkedBlockingQueue提供了以下主要方法:
- put(E e):将元素e插入队列的尾部,如果队列已满则阻塞直到有空间可用。
- offer(E e, long timeout, TimeUnit unit):将元素e插入队列的尾部,如果队列已满则阻塞一段时间,超时后返回false。
- take():获取并移除队列头部的元素,如果队列为空则阻塞直到有元素可用。
- poll(long timeout, TimeUnit unit):获取并移除队列头部的元素,如果队列为空则阻塞一段时间,超时后返回null。
- peek():获取但不移除队列头部的元素,如果队列为空则返回null。
- size():返回队列中的元素数量。
- isEmpty():判断队列是否为空。
LinkedBlockingQueue相比于ConcurrentLinkedQueue,它提供了阻塞操作,使得线程可以等待队列满或者队列空这样的条件,以便更好地控制线程的同步和协作。因此,它更适用于生产者-消费者模式和任务调度等场景。
需要注意的是,LinkedBlockingQueue的容量可以选择是否有限。可以在创建LinkedBlockingQueue对象时指定容量大小,如果不指定则默认为Integer.MAX_VALUE,即无界队列。当队列达到容量限制时,put操作将被阻塞,直到队列中有空间可用;take操作将被阻塞,直到队列中有元素可取。
总结起来,LinkedBlockingQueue是一个线程安全的阻塞队列,适用于多线程并发环境下的任务调度和协作。
LinkedBlockingQueue很好,但是如果我们需要一个分布式系统全局共享的线程安全的阻塞队列,就得换方法实现了,这边我选择的是使用redis的List数据结构实现队列。
redis队列代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 夏威夷8080
* @param <T>
*/
public class RedisBlockingQueue<T> {
private String queueKey;
private BoundListOperations<String, T> listOps;
public RedisBlockingQueue(String queueKey, RedisTemplate<String, T> redisTemplate) {
this.queueKey = queueKey;
this.listOps = redisTemplate.boundListOps(queueKey);
// 设置key和value的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(redisTemplate.getDefaultSerializer());
redisTemplate.afterPropertiesSet();
}
public void put(T item) {
listOps.leftPush(item);
}
public T take() {
while (true) {
// 从队列右侧取出元素,在这里,参数0表示阻塞时间(单位:毫秒)。
// 当队列为空时,如果设置阻塞时间为0,则表示立即返回null,而不进行等待。如果设置的阻塞时间大于0,
// 则表示在指定的时间内等待,直到队列中有可弹出的元素或超时。
//
//具体解释如下:
//
//如果队列非空,会立即将左侧的一个元素弹出并返回该元素。
//如果队列为空且阻塞时间为0,那么方法会立即返回null。
//如果队列为空且阻塞时间大于0,那么方法会在指定的时间内等待,直到队列中有可弹出的元素或超时。如果超时时仍没有可弹出的元素,则方法会返回null。
//这边的listOps.rightPop(0)被用于实现一个阻塞的队列操作,即当队列为空时,消费者线程会在此处等待,直到队列中有可弹出的元素或超时。
T item = listOps.rightPop(0, TimeUnit.MINUTES);
if (item != null) {
return item;
}
}
}
public int size() {
return Math.toIntExact(listOps.size());
}
public boolean isEmpty() {
return size() == 0;
}
}
测试类
三个线程,一个线程模拟生产者推消息,一个线程模拟消费者消费消息,其中消费的速度比生产的速度慢些,模拟队列堆积消息的场景。还有一个线程打印队列实时大小和元素数量。
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import java.util.List;
@RestController
@RequestMapping("/test")
@Api(value = "测试", tags = "测试redis队列")
@Slf4j
public class TesterController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/tt")
@ApiOperation(value = "测试redis队列")
public void tt() {
new Thread(new Runnable() {
@Override
public void run() {
// 创建RedisBlockingQueue实例
RedisBlockingQueue<String> blockingQueue = new RedisBlockingQueue<>("myQueue", redisTemplate);
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 往队列中添加元素
String a = "item" + i;
blockingQueue.put(a);
log.info("放进元素:" + a);
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 从队列中取出元素
String item = blockingQueue.take();
log.info("取出的元素:" + item);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 获取队列大小和是否为空
log.info("队列大小:" + blockingQueue.size());
log.info("队列是否为空:" + blockingQueue.isEmpty());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}).start();
}
}
升级优化为有界队列
Redis的List是一种无界队列,它可以存储任意多个元素而不受限制。所以RedisBlockingQueue并没有固定的最大容量。我们可以根据实际需求在使用时进行控制,例如通过设置最大长度限制,或者在添加元素时进行判断和处理。
下面是一个修改后的RedisBlockingQueue类的示例,通过设置最大长度来限制队列的容量:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.List;
public class RedisBlockingQueue<T> {
private String queueKey;
private BoundListOperations<String, T> listOps;
private int maxQueueSize; // 最大队列长度
public RedisBlockingQueue(String queueKey, RedisTemplate<String, T> redisTemplate, int maxQueueSize) {
this.queueKey = queueKey;
this.listOps = redisTemplate.boundListOps(queueKey);
this.maxQueueSize = maxQueueSize;
// 设置key和value的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(redisTemplate.getDefaultSerializer());
redisTemplate.afterPropertiesSet();
}
public void put(T item) throws InterruptedException {
while (listOps.size() >= maxQueueSize) {
// 队列已满,等待消费者取出元素
Thread.sleep(1000); // 可根据实际情况调整等待时间
}
listOps.lefttPush(item);
}
// 其他方法同上...
}
线程安全
通过使用Redis的leftPop或rightPop
操作,可以实现线程安全的阻塞队列。Redis的leftPop
操作是原子性的,确保每个消费者在获取元素时的互斥性,避免了并发竞争的问题。
在示例中,当队列为空时,消费者线程会调用listOps.leftPop(0)
进行阻塞等待。由于Redis的leftPop
操作是原子性的,每次只有一个线程能够成功地弹出队列中的元素。其他线程虽然也在等待,但它们会被阻塞住,直到有元素可供弹出。
因此,这种方式实现的阻塞队列是线程安全的,可以安全地在多个线程之间进行操作,保证了数据的一致性和并发安全性。
BoundListOperations介绍
BoundListOperations
是Spring Data Redis库中的一个接口,它提供了一组用于操作Redis列表(List)数据结构的方法。通过BoundListOperations
接口,我们可以方便地对Redis列表进行各种操作,而无需显式指定列表的键。
BoundListOperations
接口的实例通常通过RedisTemplate.boundListOps(key)
方法来获取,其中RedisTemplate
是Spring Data Redis库提供的Redis操作模板类,key
是Redis列表的键。
BoundListOperations
接口定义了一系列方法,包括:
-
leftPush(V value)
:将元素从左侧插入列表。 -
rightPush(V value)
:将元素从右侧插入列表。 -
leftPop()
:从左侧弹出列表中的元素。 -
rightPop()
:从右侧弹出列表中的元素。 -
size()
:获取列表的长度。 - 等等...
使用BoundListOperations
接口,我们可以更方便地操作Redis列表,而无需每次都指定列表的键。例如,在示例中,通过listOps.rightPush(item)
就可以将元素从右侧插入到Redis列表中。
总之,BoundListOperations
是Spring Data Redis库提供的一个接口,它简化了对Redis列表的操作,并提供了一系列便捷的方法来处理Redis列表。