Java API提供了一种在DelayQueue类中实现的可以用在并发应用中的数据结构。在此类中,使用激活日期存储元素,返回或从队列中提取元素的方法将忽略这些元素,这些元素数据将在后续出现 ,它们对这些方法不可见。为了获得这种行为,存储在DelayQueue类中的元素需要实现Delayed接口。这个接口可以使用延迟对象,包含getDelay()方法,返回元素激活之前的时间 。此接口强制实现如下两个方法:
- compareTo(Delayed o):Delayed接口继承Comparable接口。如果执行此方法的对象延迟小于作为参数传递的对象,则返回小于零的值。如果执行此方法的对象延迟大于作为参数传递的对象,则返回大于零的值。如果两个对象延迟相等,返回0。
- getDelay(TimeUnit unit):此方法必须返回剩余的时间,直到已过单元参数指定的激活日期。TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS和SECONDS。
在本范例中,通过在DelaydQueue类中存储具有不同激活日期的事件,学习如何使用DelaydQueue类。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为Event的类, 指定其实现Delayed接口:
public class Event implements Delayed {
-
声明名为startDate的私有Date属性:
private final Date startDate;
-
实现类构造函数,初始化属性:
public Event (Date startDate) { this.startDate=startDate; }
-
实现compareTo()方法,将Delayed对象作为参数接收,返回当前对象与参数之间延迟的差异:
@Override public int compareTo(Delayed o) { long result=this.getDelay(TimeUnit.NANOSECONDS)-o.getDelay(TimeUnit.NANOSECONDS); if (result<0) { return -1; } else if (result>0) { return 1; } return 0; }
-
实现getDelay()方法,返回对象的开始时间与实际时间之间的差异,参数为TimeUnit单位:
@Override public long getDelay(TimeUnit unit) { Date now=new Date(); long diff=startDate.getTime()-now.getTime(); return unit.convert(diff,TimeUnit.MILLISECONDS); } }
-
创建名为Task的类,指定其实现Runnable接口:
public class Task implements Runnable{
-
声明名为id的私有int属性,存储标识此任务的数字:
private final int id;
-
声明名为queue的Event类参数化的私有DelayQueue属性:
private final DelayQueue<Event> queue;
-
实现类构造函数,初始化属性:
public Task(int id, DelayQueue<Event> queue) { this.id=id; this.queue=queue; }
-
实现run()方法,首先,计算任务将要创建的事件的激活日期,然后将等于对象ID的秒数添加到实际日期:
@Override public void run() { Date now=new Date(); Date delay=new Date(); delay.setTime(now.getTime()+(id*1000)); System.out.printf("Thread %s: %s\n",id,delay);
-
使用add()方法在队列中存储100个事件:
for (int i=0; i<100; i++) { Event event=new Event(delay); queue.add(event); } } }
-
通过创建名为Main的类,添加main()方法,实现本范例主类:
public class Main { public static void main(String[] args) throws Exception{
-
创建Event类参数化的DelayQueue对象:
DelayQueue<Event> queue=new DelayQueue<>();
-
创建包含五个线程对象的数组来存储将要执行的任务:
Thread threads[]=new Thread[5];
-
创建五个不同ID的Task对象:
for (int i=0; i<threads.length; i++){ Task task=new Task(i+1, queue); threads[i]=new Thread(task); }
-
启动已创建的五个任务:
for (int i=0; i<threads.length; i++) { threads[i].start(); }
-
使用join()方法等到线程执行结束:
for (int i=0; i<threads.length; i++) { threads[i].join(); }
-
输出存储在队列中的元素到控制台。当队列长度大于零时,使用poll()方法得到Event类。如果返回null,设置主线程休眠500毫秒等到更多事件激活:
do { int counter=0; Event event; do { event=queue.poll(); if (event!=null) counter++; } while (event!=null); System.out.printf("At %s you have read %d events\n", new Date(), counter); TimeUnit.MILLISECONDS.sleep(500); } while (queue.size()>0); } }
工作原理
本节中,我们实现了Event类,此类包含一个唯一属性,事件的激活日期,且实现Delayed接口,可以在DelayQueue类中存储Event对象。
getDelay()方法返回激活日期和实际日期之间的纳秒数。两个日期军事Date类对象,使用getTime()方法返回转换为毫秒的日期。然后,将这个值转换成作为参数接收的TimeUnit。DelayQueue类的工作时间是毫微秒,在这一点上它是透明的。
如果执行compareTo()方法的对象延迟小于作为参数传递的对象,则返回小于零的值。如果延迟大于作为参数传递的对象,则返回大于零的值。如果两个对象延迟相等,返回0。
我们还实现了Task类,包含名为id的integer属性。当执行任务对象时,它将等于任务ID的秒数添加到实际日期,这个日期是此任务在DelayQueue类中存储的事件的激活日期。每个任务对象使用add()方法在队列中存储100个事件。
最后,在Main类的main()方法中,创建五个Task对象并且在对应的线程中执行。当这些线程完成执行时,使用poll()方法输出所有事件到控制台。此方法检索并删除队列的第一个元素 ,如果队列没有任何活动元素,则返回null值。在调用poll()方法时,如果返回Event类,则增加计数器。当此方法返回null值时,输出计数器值到控制台,且设置线程休眠半秒钟来等待更多的活动时间。当获得500个存储在队列中的事件时,结束程序执行。
下图显示本范例在控制台输出的部分执行信息:
可以看到当程序被激活时,如何只得到100个事件。
必须谨慎使用size()方法,此方法返回列表中元素的总数,包括活动和非活动元素。
扩展学习
DelayQueue类还有如下一些方法:
- clear():此方法删除队列的所有元素。
- Offer(E e):这里E表示用于参数化PriorityBlockingQueue类的类,它将作为参数传入的元素插入到队列中。
- peek():此方法检索但不删除队列的第一个元素。
- take():此方法检索且删除队列第一个元素,如果队列没有活动元素,执行此方法的线程将被阻塞,直到线程得到活动元素。
更多关注
- 本章“使用阻塞线程安全双端队列”小节