一次任务的实践 – 生产者消费者模式
- 任务描述:
- 该任务会调用腾讯地图接口根据经纬度来得到详细地址,但是该接口有每秒并发限制(5次/秒/接口/Key),故写一个消费者和生产者模式工具类。
- 生产者保证每六次生产,时间相差一秒(即第一次和第六次生产的时间相差一秒)。
- 消费者即可不用考虑并发限制问题,只要拿到生产者生产的产物就可以调用腾讯接口。
直接上代码
工具类代码(因为是工具类,所以消费者,生产者和仓库都放在一个文件内了)
import java.util.LinkedList;
import java.util.List;
public class tencentLbsUtils{
private static Consume consume = null;
static{
// 创建仓库对象
Warehouse warehouse = new Warehouse();
// 创建消费者对象
consume = new Consume(warehouse);
// 创建生产者线程
new Thread(new Produce(warehouse)).start();
}
/**
* 根据经纬度返回详细地址
* @param Latitude
* @param longitude
*/
public static String getDetailAddress(String Latitude, String longitude) throws InterruptedException{
// 调用消费者进行消费
return consume.getDetailAddress(Latitude, longitude);
}
}
/**
* 消费者
*/
class Consume{
public Warehouse producer;
Consume(Warehouse producer){
this.producer = producer;
}
public void consume() throws InterruptedException{
System.out.println("开始消费");
producer.consume();
}
public String getDetailAddress(String Latitude, String longitude) throws InterruptedException{
// 访问仓库是否可以进行消费
consume();
// 调用接口
System.out.println("调用地图接口");
// 返回详细地址
return "详细地址。。。";
}
}
/**
* 生产者
*/
class Produce implements Runnable{
private Warehouse producer;
Produce(Warehouse producer){
this.producer = producer;
}
@Override
public void run() {
// 让生产者循环生产
while(true){
try {
System.out.println("开始生产");
producer.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 仓库
*/
class Warehouse{
/**
* 仓库当前容量
*/
private int index = 5;
/**
* 最大容量
*/
private final int MAX = 5;
/**
* 理论上最大只会存放5个
*/
private List<Long> timeList = new LinkedList<>();
public synchronized void produce() throws InterruptedException{
while(index >= MAX){
System.out.println("等待消費。。。");
this.wait();
}
// 拿到前面第五个的消费时间的后一秒
long upTime = timeList.remove(0) + 1000;
long currentTime = System.currentTimeMillis();
// 如果前面第五个消费时间的后一秒大于当前时间
while(upTime > currentTime){
// 等待时间差,保证每六个之间前后的时间差为一秒
this.wait(upTime - currentTime);
// 刷新当前时间,需要重新判断,排除是notify 或 notifyAll方法激活的该线程
currentTime = System.currentTimeMillis();
}
// 生产一个
this.index++;
//生产之后可通知消费者线程消费
notifyAll();
}
public synchronized void consume() throws InterruptedException{
// 判断仓库空了,则等待。
while(index <= 0) {
System.out.println("等待生產。。。");
this.wait();
}
// 消费一个
index--;
// 记录当前消费时间
timeList.add(System.currentTimeMillis());
// 消费之后可通知生产者线程进行生产
notifyAll();
}
}
测试主程序
import java.text.SimpleDateFormat;
public class Test{
public static void main(String[] args) throws Exception{
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
for(int i = 1; i <= 20; i++){
// 调用接口
tencentLbsUtils.getDetailAddress("经度","纬度");
// 打印时间
System.out.println("第 " + i + " 次调用接口,时间: " + sdf.format(System.currentTimeMillis()));
}
}
}
执行结果
开始消费
调用地图接口
第 1 次调用接口,时间: 15:48:41.419
开始消费
调用地图接口
第 2 次调用接口,时间: 15:48:41.421
开始消费
调用地图接口
第 3 次调用接口,时间: 15:48:41.421
开始消费
调用地图接口
第 4 次调用接口,时间: 15:48:41.422
开始消费
调用地图接口
第 5 次调用接口,时间: 15:48:41.422
开始消费
等待生產。。。
开始生产
开始生产
调用地图接口
第 6 次调用接口,时间: 15:48:42.420
开始消费
等待生產。。。
调用地图接口
第 7 次调用接口,时间: 15:48:42.421
开始消费
等待生產。。。
开始生产
开始生产
开始生产
开始生产
调用地图接口
第 8 次调用接口,时间: 15:48:42.422
开始消费
调用地图接口
第 9 次调用接口,时间: 15:48:42.422
开始消费
调用地图接口
第 10 次调用接口,时间: 15:48:42.423
开始消费
等待生產。。。
开始生产
调用地图接口
第 11 次调用接口,时间: 15:48:43.421
开始消费
等待生產。。。
开始生产
调用地图接口
第 12 次调用接口,时间: 15:48:43.421
开始消费
等待生產。。。
开始生产
开始生产
开始生产
调用地图接口
第 13 次调用接口,时间: 15:48:43.423
开始消费
调用地图接口
第 14 次调用接口,时间: 15:48:43.423
开始消费
调用地图接口
第 15 次调用接口,时间: 15:48:43.423
开始消费
等待生產。。。
开始生产
调用地图接口
第 16 次调用接口,时间: 15:48:44.420
开始消费
等待生產。。。
开始生产
调用地图接口
第 17 次调用接口,时间: 15:48:44.421
开始消费
等待生產。。。
调用地图接口
开始生产
第 18 次调用接口,时间: 15:48:44.424
开始消费
等待生產。。。
调用地图接口
第 19 次调用接口,时间: 15:48:44.424
开始消费
等待生產。。。
开始生产
开始生产
调用地图接口
第 20 次调用接口,时间: 15:48:44.425
开始消费
等待生產。。。
其他说明
-
wait(long i)方法和Tread.sleep(long i) wait方法会释放对象锁,sleep方法不会释放对象锁,如果一个线程在仓库内使用了sleep方法会导致其他线程也进入等待。
-
long upTime = timeList.remove(0)+1000; 该行可能存在偏差0.001毫秒左右,如果有必要,可以 +1001或+1002
-
notify()方法只会唤醒在这个对象上阻塞的其中一个线程,如有多个生产者或消费者,应使用notifyAll()方法
-
上面的代码异常应该按需要进行捕捉