List被称为最基本的集合,具有不确定数量的元素,可以在任何位置增加、读取,或者删除元素。并发列表允许不同线程在同一时间添加或删除来自列表中的元素,而不会产生任何数据非一致性错误。与列表相似的,还有双端队列,其数据结构与队列相近,但在双端队列中,可以在前(头)后(尾)端添加或删除。
本节将学习在并发应用中如何使用非阻塞双端队列。非阻塞双端队列提供的操作如果没有马上执行完(例如想要得到空列表中的一个元素),将根据操作不同抛出异常或者返回null值。Java 7引入ConcurrentLinkedDeque类实现非阻塞并发双端队列。
将要实现的范例中包含如下两个不同任务:
- 向双端队列添加大量元素
- 从双端队列中删除数据
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤实现范例:
-
创建名为AddTask的类,指定其实现Runnable接口:
public class AddTask implements Runnable{
-
声明名为list的String类参数化的私有ConcurrentLinkedDeque属性:
private final ConcurrentLinkedDeque<String> list;
-
实现类构造函数,初始化属性:
public AddTask(ConcurrentLinkedDeque<String> list) { this.list = list; }
-
实现类的run()方法,循环10000次,在每个循环中,获取双端队列的第一个和最后一个元素,所以将得到总计10000个元素:
@Override public void run() { String name=Thread.currentThread().getName(); for (int i=0; i<10000; i++){ list.add(name+": Element "+i); } }
-
创建名为PollTask的类,指定其实现Runnable接口:
public class PollTask implements Runnable{
-
声明名为list的String类参数化的私有ConcurrentLinkedDeque属性:
private final ConcurrentLinkedDeque<String> list;
-
实现类构造函数,初始化属性:
public PollTask(ConcurrentLinkedDeque<String> list) { this.list = list; }
-
实现类的run()方法,通过循环5000次取出双端队列10000个元素,每次取头和尾两个元素:
@Override public void run() { for (int i=0; i<5000; i++) { list.pollFirst(); list.pollLast(); } }
-
通过创建名为Main的类,添加main()方法,实现本范例主类:
public class Main { public static void main(String[] args) {
-
创建名为list的String类参数化的私有ConcurrentLinkedDeque属性:
ConcurrentLinkedDeque<String> list=new ConcurrentLinkedDeque<>();
-
创建名为threads的数组,包含100个Thread对象:
Thread threads[]=new Thread[100];
-
创建100个AddTask对象以及运行这些对象的线程,将线程存储到已创建好的数组中并启动:
for (int i=0; i<threads.length ; i++){ AddTask task=new AddTask(list); threads[i]=new Thread(task); threads[i].start(); } System.out.printf("Main: %d AddTask threads have been launched\n",threads.length);
-
使用join()方法等待线程执行完成:
for (int i=0; i<threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
输出列表长度到控制台:
System.out.printf("Main: Size of the List: %d\n",list.size());
-
创建100个PollTask对象以及运行这些对象的线程,将线程存储到已创建好的数组中并启动:
for (int i=0; i< threads.length; i++){ PollTask task=new PollTask(list); threads[i]=new Thread(task); threads[i].start(); } System.out.printf("Main: %d PollTask threads have been launched\n", threads.length);
-
使用join()方法等待线程执行完成:
for (int i=0; i<threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
输出列表长度到控制台:
System.out.printf("Main: Size of the List: %d\n",list.size()); } }
工作原理
在本节中,使用String类参数化的ConcurrentLinkedDeque对象来处理数据的非阻塞并发双端队列。下图显示本范例在控制台输出的执行信息:
首先,执行100个AddTask任务来添加元素到列表,每个任务都使用add()方法向列表中插入10000个元素,此方法在双端队列尾部添加新元素。当所有任务完成时,输出双端队列的元素总数到控制台,此时双端队列有1000000个元素。
然后,执行100个PollTGask任务从双端队列中删除元素。每个任务都使用pollFirst()和pollLast()方法从双端队列中删除10000个元素。pollFirst()方法返回和删除双端队列的第一个元素,pollLast()方法返回和删除双端队列的最后一个元素。如果双端队列为空,则返回null值。当所有任务完成时,输出双端队列的元素总数到控制台,此时考虑到ConcurrentLinkedDeque结构不允许列表添加null值,列表具有零个元素。
使用size()方法输出双端队列的元素数量到控制台,需要注意的是此方法返回的值不是准确的,尤其当有线程在列表中添加或删除数据时使用此方法。这个方法必须遍历整个双端队列来计算元素数量,并且此操作能够更改列表的内容。 只有当没有线程修改双端队列时使用此方法,才能保证返回的结果是正确的。
扩展学习
ConcurrentLinkedDeque类还有很多从双端队列中获取元素的方法:
- getFirst()和getLast():这两个方法分别返回双端队列的第一个和最后一个元素,不删除从双端队列中返回的元素。如果双端队列为空,则抛出NoSuchElementExcpetion异常。
- peek()、peekFirst()和peekLast() :这些方法分别返回双端队列的第一个和最后一个元素,不删除从双端队列中返回的元素。如果双端队列为空,返回null值。
- remove()、removeFirst()和removeLast():这些方法分别返回双端队列的第一个和最后一个元素,删除从双端队列中返回的元素。如果双端队列为空,则抛出NoSuchElementExcpetion异常。