在本章中,我们将介绍:
- 使用非阻塞线程安全列表
- 使用阻塞线程安全列表
- 使用按优先级排序的阻塞线程安全列表
- 使用具有延迟元素的线程安全列表
- 使用线程安全的可导航地图
- 生成并发随机数
- 使用原子变量
- 使用原子阵列
数据结构是编程的基本元素。几乎每个程序都使用一种或多种类型的数据结构来存储和管理数据。Java API 提供了包含接口、类和算法的Java 集合框架,这些框架实现了许多不同的数据结构,可以在程序中使用。
当您需要在并发程序中处理数据收集时,必须非常小心地选择实现。大多数集合类不准备与并发应用一起工作,因为它们不控制对其数据的并发访问。如果某些并发任务共享的数据结构不适合处理并发任务,则可能会出现数据不一致错误,从而影响程序的正确操作。这种数据结构的一个例子是ArrayList
类。
Java 提供了可以在并发程序中使用的数据集合,而不会出现任何问题或不一致。Java 基本上提供了两种在并发应用中使用的集合:
- 阻塞收集:这种类型的收集包括添加和删除数据的操作。如果由于集合已满或为空而无法立即执行该操作,则发出调用的线程将被阻止,直到可以执行该操作为止。
- 非阻塞收集:这种收集还包括添加和删除数据的操作。如果不能立即执行该操作,则该操作返回一个
null
值或抛出一个异常,但发出调用的线程不会被阻塞。
通过本章的介绍,您将学习如何在并发应用中使用一些 Java 集合。这包括:
- 非阻塞列表,使用
ConcurrentLinkedDeque
类 - 阻止列表,使用
LinkedBlockingDeque
类 - 使用
LinkedTransferQueue
类与数据的生产者和消费者一起使用的阻止列表 - 按优先级排列元素的阻止列表,带有
PriorityBlockingQueue
- 带有延迟元素的阻塞列表,使用
DelayQueue
类 - 非阻塞导航地图,使用
ConcurrentSkipListMap
类 - 随机数,使用
ThreadLocalRandom
类 - 原子变量,使用
AtomicLong
和AtomicIntegerArray
类
最基本的收藏是列表。列表的元素数量不确定,您可以添加、读取或删除任何位置的元素。并发列表允许各个线程一次添加或删除列表中的元素,而不会产生任何数据不一致。
在本食谱中,您将学习如何在并发程序中使用非阻塞列表。非阻塞列表提供的操作,如果操作无法立即完成(例如,您希望获取列表中的一个元素,而列表为空),它们会抛出异常或返回一个null
值,具体取决于操作。Java7 引入了实现非阻塞并发列表的ConcurrentLinkedDeque
类。
我们将用以下两个不同的任务实现一个示例:
- 向列表中大量添加数据的程序
- 从同一个列表中大量删除数据的方法
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
AddTask
的类,并指定它实现Runnable
接口。public class AddTask implements Runnable {
-
声明一个私有的
ConcurrentLinkedDeque
属性,该属性由名为list
的String
类参数化。private ConcurrentLinkedDeque<String> list;
-
实现类的构造函数以初始化其属性。
public AddTask(ConcurrentLinkedDeque<String> list) { this.list=list; }
-
实现类的
run()
方法。它将在列表中存储 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 {
-
声明一个私有的
ConcurrentLinkedDeque
属性,该属性由名为list
的String
类参数化。private 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
对象。
```java
ConcurrentLinkedDeque<String> list=new ConcurrentLinkedDeque<>();
```
- 为 100 个名为
threads
的Thread
对象创建一个数组。
```java
Thread threads[]=new Thread[100];
```
- 创建 100 个
AddTask
对象和一个线程来运行每个对象。在先前创建的数组中存储每个线程并启动线程。
```java
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()
方法等待线程完成。
```java
for (int i=0; i<threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 在控制台中写入列表的大小。
```java
System.out.printf("Main: Size of the List: %d\n",list.size());
```
- 创建 100 个
PollTask
对象和一个线程来运行每个对象。在先前创建的数组中存储每个线程并启动线程。
```java
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()
方法等待线程的终结。
```java
for (int i=0; i<threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 在控制台中写入列表的大小。
```java
System.out.printf("Main: Size of the List: %d\n",list.size());
```
在这个方法中,我们使用了用String
类参数化的ConcurrentLinkedDeque
对象来处理非阻塞并发数据列表。以下屏幕截图显示了此示例的执行输出:
首先,您已经执行了 100 个AddTask
任务,将元素添加到列表中。每个任务都使用add()
方法向列表中插入 10000 个元素。此方法将新元素添加到列表的末尾。当所有这些任务完成后,您已经在控制台中写入了列表的元素数。此时,列表中有 1000000 个元素。
然后,您已经执行了 100 个PollTask
任务以从列表中删除元素。这些任务中的每一个都使用pollFirst()
和pollLast()
方法删除列表中的 10000 个元素。pollFirst()
方法返回并移除列表的第一个元素,pollLast()
方法返回并移除列表的最后一个元素。如果列表为空,则这些方法返回一个null
值。当所有这些任务完成后,您已经在控制台中写入了列表的元素数。此时,列表中没有元素。
要写入列表的元素数,您使用了size()
方法。您必须考虑此方法可能返回一个非实数的值,特别是当有线程在列表中添加或删除数据时使用此方法。该方法必须遍历整个列表以计数元素,并且该操作可能会更改列表的内容。只有在没有任何线程修改列表时使用它们,才能保证返回的结果是正确的。
ConcurrentLinkedDeque
类提供了更多从列表中获取元素的方法:
getFirst()
和getLast()
:这些方法分别返回列表中的第一个和最后一个元素。它们不会从列表中删除返回的元素。如果列表为空,这些方法将抛出一个NoSuchElementExcpetion
异常。peek()
、peekFirst()
和peekLast()
:这些方法分别返回列表的第一个和最后一个元素。他们不会从列表中删除返回的元素。如果列表为空,则这些方法返回一个null
值。removeFirst()
和的【最后一个元素】分别返回。他们从列表中删除返回的元素。如果列表为空,这些方法将抛出NoSuchElementException
异常。
最基本的集合是列表。列表中的元素数量不确定,您可以在任何位置添加、读取或删除元素。并发列表允许各种线程一次添加或删除列表中的元素,而不会产生任何数据不一致。
在本食谱中,您将学习如何在并发程序中使用阻止列表。阻塞列表和非阻塞列表之间的主要区别在于,阻塞列表具有插入和删除元素的方法,如果由于列表已满或为空而无法立即执行操作,则会阻塞发出调用的线程,直到可以执行操作为止。Java 包含实现阻塞列表的LinkedBlockingDeque
类。
您将使用以下两个任务实现一个示例:
- 向列表中大量添加数据的程序
- 从同一个列表中大量删除数据的方法
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照下面描述的步骤实施示例:
-
创建一个名为
Client
的类,并指定它实现Runnable
接口。public class Client implements Runnable{
-
声明一个私有的
LinkedBlockingDeque
属性,该属性由名为requestList
的String
类参数化。private LinkedBlockingDeque<String> requestList;
-
实现类的构造函数以初始化其属性。
public Client (LinkedBlockingDeque<String> requestList) { this.requestList=requestList; }
-
执行
run()
方法。使用requestList object
的put()
方法,每秒在列表中插入五个String
对象。重复这个循环三次。@Override public void run() { for (int i=0; i<3; i++) { for (int j=0; j<5; j++) { StringBuilder request=new StringBuilder(); request.append(i); request.append(":"); request.append(j); try { requestList.put(request.toString()); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("Client: %s at %s.\n",request,new Date()); } try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("Client: End.\n"); }
-
通过创建名为
Main
的类来创建示例的主类,并向其添加main()
方法。public class Main { public static void main(String[] args) throws Exception {
-
声明并创建用名为
list
的String
类参数化的LinkedBlockingDeque
。LinkedBlockingDeque<String> list=new LinkedBlockingDeque<>(3);
-
创建并启动
Thread
对象以执行客户端任务。Client client=new Client(list); Thread thread=new Thread(client); thread.start();
-
使用列表对象的
take()
方法每隔 300 毫秒获取三个列表String
对象。重复这个循环五次。在控制台中编写字符串。for (int i=0; i<5 ; i++) { for (int j=0; j<3; j++) { String request=list.take(); System.out.printf("Main: Request: %s at %s. Size: %d\n",request,new Date(),list.size()); } TimeUnit.MILLISECONDS.sleep(300); }
-
编写一条消息以指示程序结束。
System.out.printf("Main: End of the program.\n");
在这个方法中,您使用了用String
类参数化的LinkedBlockingDeque
来处理非阻塞并发数据列表。
Client
类使用put()
方法在列表中插入字符串。如果列表已满(因为您使用固定容量创建了该列表),则该方法将阻止其线程的执行,直到列表中有一个空白为止。
Main
类使用take()
方法从列表中获取字符串。如果列表为空,则该方法将阻止其线程的执行,直到列表中有元素为止。
本例中使用的LinkedBlockingDeque
类的两个方法如果在被阻止时被中断,都会抛出InterruptedException
异常,因此必须包含捕获该异常所需的代码。
LinkedBlockingDeque
类还提供了从列表中放置和获取元素的方法,这些元素不是阻塞,而是抛出异常或返回null
值。这些方法是:
takeFirst()
和takeLast()
:这些方法分别返回列表的第一个和最后一个元素。它们从列表中删除返回的元素。如果列表为空,这些方法将阻塞线程,直到列表中有元素为止。getFirst()
和getLast()
:这些方法分别返回列表中的第一个和最后一个元素。它们不会从列表中删除返回的元素。如果列表为空,这些方法将抛出一个NoSuchElementExcpetion
异常。peek()
、peekFirst()
和peekLast()
:这些方法分别返回列表的第一个和最后一个元素。他们不会从列表中删除返回的元素。如果列表为空,则这些方法返回一个null
值。poll()
、pollFirst()
和pollLast()
:这些方法分别返回列表的第一个和最后一个元素。他们从列表中删除返回的元素。如果列表为空,则这些方法返回一个null
值。add()
、addFirst()
、addLast()
:这些方法分别在的第一个和最后一个位置添加元素。如果列表已满(您使用固定容量创建了它),这些方法将抛出一个IllegalStateException
异常。
- 第 6 章并发集合中的使用非阻塞线程安全列表配方
处理数据结构时的一个典型需求是拥有一个有序列表。Java 提供了具有此功能的PriorityBlockingQueue
。
要添加到PriorityBlockingQueue
的所有元素都必须实现Comparable
接口。这个接口有一个方法,compareTo()
接收相同类型的对象,因此您有两个对象要比较:一个是执行该方法的对象,另一个是作为参数接收的对象。如果局部对象小于参数,则方法必须返回小于零的数字;如果局部对象大于参数,则方法必须返回大于零的数字;如果两个对象相等,则方法必须返回零的数字。
PriorityBlockingQueue
在插入元素时使用compareTo()
方法确定插入元素的位置。较大的元素将是队列的尾部。
PriorityBlockingQueue
的另一个重要特征是它是一个阻塞数据结构。它有一些方法,如果它们不能立即执行操作,就阻塞线程,直到它们能够执行为止。
在本食谱中,您将学习如何使用PriorityBlockingQueue
类实现一个示例,在该示例中,您将在同一个列表中存储许多具有不同优先级的事件,以检查队列是否按您想要的顺序排列。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Event
的类,并指定它实现了用Event
类参数化的Comparable
接口。public class Event implements Comparable<Event> {
-
声明一个名为
thread
的私有int
属性,以存储创建事件的线程的编号。private int thread;
-
声明一个名为
priority
的私有int
属性来存储事件的优先级。private int priority;
-
实现类的构造函数以初始化其属性。
public Event(int thread, int priority){ this.thread=thread; this.priority=priority; }
-
执行
getThread()
方法返回线程属性的值。public int getThread() { return thread; }
-
执行
getPriority()
方法返回优先级属性的值。public int getPriority() { return priority; }
-
执行
compareTo()
方法。将Event
作为参数接收,并将当前事件的优先级与作为参数接收的优先级进行比较。如果当前事件的优先级较大,则返回-1
,如果两个优先级相等,则返回0
,如果当前事件的优先级较小,则返回1
。请注意,这与大多数Comparator.compareTo()
实现相反。@Override public int compareTo(Event e) { if (this.priority>e.getPriority()) { return -1; } else if (this.priority<e.getPriority()) { return 1; } else { return 0; } }
-
创建一个名为
Task
的类,并指定它实现Runnable
接口。public class Task implements Runnable {
-
声明一个名为
id
的私有int
属性来存储标识任务的编号。private int id;
-
声明一个名为
queue
的Event
类参数化的私有PriorityBlockingQueue
属性,用于存储任务生成的事件。
```java
private PriorityBlockingQueue<Event> queue;
```
- 实现类的构造函数以初始化其属性。
```java
public Task(int id, PriorityBlockingQueue<Event> queue) {
this.id=id;
this.queue=queue;
}
```
- 执行
run()
方法。它在队列中存储 1000 个事件,使用其 ID 标识创建该事件的任务,并将其作为优先级递增。使用add()
方法将事件存储在队列中。
```java
@Override
public void run() {
for (int i=0; i<1000; i++){
Event event=new Event(id,i);
queue.add(event);
}
}
```
- 通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。
```java
public class Main{
public static void main(String[] args) {
```
- 创建一个名为
queue
的Event
类参数化的PriorityBlockingQueue
对象。
```java
PriorityBlockingQueue<Event> queue=new PriorityBlockingQueue<>();
```
- 创建一个包含五个
Thread
对象的数组来存储将要执行五个任务的线程。
```java
Thread taskThreads[]=new Thread[5];
```
- 创建五个
Task
对象。将线程存储在先前创建的数组中。
```java
for (int i=0; i<taskThreads.length; i++){
Task task=new Task(i,queue);
taskThreads[i]=new Thread(task);
}
```
- 启动前面创建的五个线程。
```java
for (int i=0; i<taskThreads.length ; i++) {
taskThreads[i].start();
}
```
- 等待使用
join()
方法完成五个线程。
```java
for (int i=0; i<taskThreads.length ; i++) {
try {
taskThreads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 向控制台写入队列的实际大小和其中存储的事件。使用
poll()
方法从队列中删除事件。
```java
System.out.printf("Main: Queue Size: %d\n",queue.size());
for (int i=0; i<taskThreads.length*1000; i++){
Event event=queue.poll();
System.out.printf("Thread %s: Priority %d\n",event.getThread(),event.getPriority());
}
```
- 使用队列的最终大小向控制台写入消息。
```java
System.out.printf("Main: Queue Size: %d\n",queue.size());
System.out.printf("Main: End of the program\n");
```
在本例中,您已经使用PriorityBlockingQueue
实现了Event
对象的优先级队列。正如我们在介绍中提到的,PriorityBlockingQueue
中存储的所有元素都必须实现Comparable
接口,所以您已经在事件类中实现了compareTo()
方法。
所有事件都具有优先级属性。优先级值较高的元素将是队列中的第一个元素。当您实现了compareTo()
方法时,如果执行该方法的事件的优先级高于作为参数传递的事件的优先级,则返回-1
作为结果。在另一种情况下,如果执行方法的事件的优先级低于作为参数传递的事件的优先级,则返回1
作为结果。如果两个对象具有相同的优先级,compareTo()
方法返回0
值。在这种情况下,PriorityBlockingQueue
类不能保证元素的顺序。
我们已经实现了Task
类,将Event
对象添加到优先级队列中。每个任务对象使用add()
方法向队列添加 1000 个事件,优先级在 0 到 999 之间。
Main
类的main()
方法创建五个Task
对象,并在相应的线程中执行它们。当所有线程完成执行后,您已经将所有元素写入控制台。为了从队列中获取元素,我们使用了poll()
方法。该方法返回并从队列中删除第一个元素。
以下屏幕截图显示了程序执行的部分输出:
您可以看到队列如何具有 5000 个元素的大小,以及第一个元素如何具有最大的优先级值。
PriorityBlockingQueue
类还有其他有趣的方法。以下是对其中一些项目的说明:
clear()
:此方法删除队列中的所有元素。take()
:此方法返回并移除队列的第一个元素。如果队列为空,则该方法将阻塞其线程,直到队列包含元素。put(E``e)``E
是用于参数化PriorityBlockingQueue
类的类。此方法将作为参数传递的元素插入队列。peek()
:此方法返回队列的第一个元素,但不删除它。
- 第 6 章并发集合中的使用阻塞线程安全列表配方
在DelayedQueue
类中实现了 Java API 提供的一个有趣的数据结构,您可以在并发应用中使用它。在这个类中,您可以存储带有激活日期的元素。返回或提取队列元素的方法将忽略其数据在将来的元素。这些方法看不见它们。
要获得此行为,您要存储在DelayedQueue
类中的元素必须实现Delayed
接口。此接口允许您处理延迟对象,因此您将实现DelayedQueue
类中存储的对象的激活日期,作为激活日期之前的剩余时间。此接口强制实现以下两种方法:
compareTo(Delayed``o)
:Delayed
接口扩展了Comparable
接口。如果执行方法的对象的延迟小于作为参数传递的对象,则此方法将返回小于零的值;如果执行方法的对象的延迟大于作为参数传递的对象,则返回大于零的值;如果两个对象的延迟相同,则返回零值。getDelay(TimeUnit``unit)
:此方法必须返回单位参数指定单位中激活日期之前的剩余时间。TimeUnit
类是一个枚举,具有以下常量:DAYS
、HOURS
、MICROSECONDS
、MILLISECONDS
、MINUTES
、NANOSECONDS
和SECONDS
。
在本例中,您将学习如何使用DelayedQueue
类存储一些具有不同激活日期的事件。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Event
的类,并指定它实现Delayed
接口。public class Event implements Delayed {
-
声明名为
startDate
的私有Date
属性。private 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()
方法。返回对象的startDate
与接收到的TimeUnit
中实际Date
的差值作为参数。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 int id;
-
声明一个私有的
DelayQueue
属性,该属性由名为queue
的Event
类参数化。private DelayQueue<Event> queue;
-
实现类的构造函数以初始化其属性。
public Task(int id, DelayQueue<Event> queue) { this.id=id; this.queue=queue; }
-
执行
run()
方法。首先,计算此任务将要创建的事件的激活日期。将等于对象 ID 的秒数添加到实际日期。
```java
@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 个事件。
```java
for (int i=0; i<100; i++) {
Event event=new Event(delay);
queue.add(event);
}
}
```
- 通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。
```java
public class Main {
public static void main(String[] args) throws Exception {
```
- 创建一个用
Event
类参数化的DelayedQueue
对象。
```java
DelayQueue<Event> queue=new DelayQueue<>();
```
- 创建一个包含五个
Thread
对象的数组来存储要执行的任务。
```java
Thread threads[]=new Thread[5];
```
- 创建五个具有不同 ID 的
Task
对象。
```java
for (int i=0; i<threads.length; i++){
Task task=new Task(i+1, queue);
threads[i]=new Thread(task);
}
```
- 启动之前创建的所有五个任务。
```java
for (int i=0; i<threads.length; i++) {
threads[i].start();
}
```
- 使用
join()
方法等待线程的终结。
```java
for (int i=0; i<threads.length; i++) {
threads[i].join();
}
```
- 将队列中存储的事件写入控制台。当队列大小大于零时,使用
poll()
方法获取Event
类。如果返回null
,则将主线程放置 500 毫秒,等待更多事件的激活。
```java
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
接口,因此您可以将Event
对象存储在DelayedQueue
类中。
getDelay()
方法返回激活日期和实际日期之间的纳秒数。这两个日期都是Date
类的对象。您使用了返回转换为毫秒的日期的getTime()
方法,然后将该值转换为作为参数接收的TimeUnit
。DelayedQueue
类的工作时间为纳秒,但在这一点上,它对您来说是透明的。
compareTo()
方法如果执行方法的对象的延迟小于作为参数传递的对象的延迟,则返回小于零的值;如果执行方法的对象的延迟大于作为参数传递的对象的延迟,则返回大于零的值;并且返回值0
如果两个延迟相等。
您还实现了Task
类。此类有一个名为id
的integer
属性。当执行一个Task
对象时,它将等于任务 ID 的秒数添加到实际日期,即该任务在DelayedQueue
类中存储的事件的激活日期。每个Task
对象使用add()
方法在队列中存储 100 个事件。
最后,在Main
类的main()
方法中,您已经创建了五个Task
对象,并在相应的线程中执行它们。当这些线程完成执行时,您已经使用poll()
方法将所有事件写入控制台。该方法检索并删除队列的第一个元素。如果队列没有任何活动元素,则该方法返回null
值。您调用了poll()
方法,如果它返回一个Event
类,则递增一个计数器。当poll()
方法返回null
值时,您在控制台中写入计数器的值,并在半秒钟内使线程休眠,以等待更多活动事件。获得队列中存储的 500 个事件后,程序的执行完成。
以下屏幕截图显示了程序执行的部分输出:
您可以看到程序如何在激活时仅获取 100 个事件。
你必须非常小心使用size()
方法。它返回列表中包含活动和非活动元素的元素总数。
DelayQueue
类还有其他有趣的方法,如下所示:
clear()
:此方法删除队列的所有元素。offer(E``e)
:E
表示用于参数化DelayQueue
类的类。此方法在队列中插入作为参数传递的元素。peek()
:此方法检索,但不删除队列的第一个元素。take()
:此方法检索并删除队列的第一个元素。如果队列中没有任何活动元素,则执行该方法的线程将被阻塞,直到该线程具有一些活动元素为止。
- 第 6 章并发集合中的使用阻塞线程安全列表配方
由 Java API 提供的一个有趣的数据结构,您可以在并发程序中使用,它由ConcurrentNavigableMap
接口定义。实现ConcurrentNavigableMap
接口的类将元素存储在两个部分中:
- 唯一标识元素的键
- 定义元素的其余数据
每个部分必须在不同的类中实现。
Java API 还提供了一个实现该接口的类,即实现具有ConcurrentNavigableMap
接口行为的非阻塞列表的ConcurrentSkipListMap
接口。内部使用跳过列表存储数据。跳过列表是一种基于并行列表的数据结构,它允许我们获得类似于二叉树的效率。使用它,您可以获得一个排序数据结构,与排序列表相比,它具有更好的插入、搜索或删除元素的访问时间。
跳过列表由 William Pugh 于 1990 年引入。
当您在映射中插入一个元素时,它使用键对它们进行排序,因此所有元素都将被排序。除了返回具体元素的方法外,该类还提供获取映射子映射的方法。
在本食谱中,您将学习如何使用ConcurrentSkipListMap
类实现联系人映射。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Contact
的类。public class Contact {
-
声明两个名为
name
和phone
的私有String
属性。private String name; private String phone;
-
实现类的构造函数以初始化其属性。
public Contact(String name, String phone) { this.name=name; this.phone=phone; }
-
实现返回
name
和phone
属性值的方法。public String getName() { return name; } public String getPhone() { return phone; }
-
创建一个名为
Task
的类,并指定它实现Runnable
接口。public class Task implements Runnable {
-
声明一个私有的
ConcurrentSkipListMap
属性,该属性由名为map
的String
和Contact
类参数化。private ConcurrentSkipListMap<String, Contact> map;
-
声明一个名为
id
的私有String
属性来存储当前任务的 ID。private String id;
-
实现类的构造函数以存储其属性。
public Task (ConcurrentSkipListMap<String, Contact> map, String id) { this.id=id; this.map=map; }
-
执行
run()
方法。它使用任务 ID 和增量数字在地图中存储 1000 个不同的联系人,以创建Contact
对象。使用put()
方法在地图中存储联系人。@Override public void run() { for (int i=0; i<1000; i++) { Contact contact=new Contact(id, String.valueOf(i+1000)); map.put(id+contact.getPhone(), contact); } }
-
通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个用名为
map
的String
和Conctact
类参数化的ConcurrentSkipListMap
对象。
```java
ConcurrentSkipListMap<String, Contact> map;
map=new ConcurrentSkipListMap<>();
```
- 为 25
Thread
对象创建一个数组,以存储要执行的所有Task
对象。
```java
Thread threads[]=new Thread[25];
int counter=0;
```
- 创建并启动 25 个任务对象,将大写字母指定为每个任务的 ID。
```java
for (char i='A'; i<'Z'; i++) {
Task task=new Task(map, String.valueOf(i));
threads[counter]=new Thread(task);
threads[counter].start();
counter++;
}
```
- 使用
join()
方法等待线程的终结。
```java
for (int i=0; i<25; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 使用
firstEntry()
方法获取地图的第一个条目。将其数据写入控制台。
```java
System.out.printf("Main: Size of the map: %d\n",map.size());
Map.Entry<String, Contact> element;
Contact contact;
element=map.firstEntry();
contact=element.getValue();
System.out.printf("Main: First Entry: %s: %s\n",contact.getName(),contact.getPhone());
```
- 使用
lastEntry()
方法获取地图的最后一个条目。将其数据写入控制台。
```java
element=map.lastEntry();
contact=element.getValue();
System.out.printf("Main: Last Entry: %s: %s\n",contact.getName(),contact.getPhone());
```
- 使用
subMap()
方法获取地图的子地图。将其数据写入控制台。
```java
System.out.printf("Main: Submap from A1996 to B1002: \n");
ConcurrentNavigableMap<String, Contact> submap=map.subMap("A1996", "B1002");
do {
element=submap.pollFirstEntry();
if (element!=null) {
contact=element.getValue();
System.out.printf("%s: %s\n",contact.getName(),contact.getPhone());
}
} while (element!=null);
}
```
在这个配方中,我们已经实现了一个Task
类来在导航地图中存储Contact
对象。每个联系人都有一个名称(创建该联系人的任务的 ID)和一个电话号码(介于 1000 和 2000 之间)。我们使用这些值的串联作为联系人的键。每个Task
对象创建 1000 个联系人,这些联系人使用put()
方法存储在导航地图中。
如果插入的元素具有映射中存在的键,则与该键关联的元素将替换为新元素。
Main
类的main()
方法创建了 25 个Task
对象,使用 ID 作为 A 和 Z 之间的字母。然后,您使用了一些方法从地图中获取数据。firstEntry()
方法返回一个包含地图第一个元素的Map.Entry
对象。此方法不会从映射中删除元素。该对象包含键和元素。为了获得元素,您调用了getValue()
方法。您可以使用getKey()
方法获取该元素的密钥。
lastEntry()
方法返回带有地图最后一个元素的Map.Entry
对象,subMap()
方法返回带有地图部分元素的ConcurrentNavigableMap
对象,在这种情况下,键在A1996
和B1002
之间的元素。在本例中,您使用了pollFirst()
方法来处理subMap()
方法的元素。该方法返回并删除 submap 的第一个Map.Entry
对象。
以下屏幕截图显示了程序执行的输出:
ConcurrentSkipListMap
类还有其他有趣的方法。以下是其中一些:
headMap(K``toKey)
:K
是用于ConcurrentSkipListMap
对象参数化的键值类。此方法返回映射的第一个元素的子映射,这些元素的键小于作为参数传递的键。tailMap(K``fromKey)
:K
是用于ConcurrentSkipListMap
对象参数化的键值类。此方法返回映射的最后一个元素的子映射,这些元素的键大于作为参数传递的键。putIfAbsent(K``key,``V``Value)
:此方法插入指定为参数的值,如果该键在地图中不存在,则将该键指定为参数。pollLastEntry()
:此方法返回并移除带有地图最后一个元素的Map.Entry
对象。replace(K``key,``V``Value)
:此方法替换与指定为参数的键相关的值,如果该键存在于地图中。
- 第 6 章并发集合中的使用非阻塞线程安全列表配方
Java 并发 API 提供了在并发应用中生成伪随机数的特定类。它是ThreadLocalRandom
类,在 Java7 版本中是新的。它作为线程局部变量工作。每个想要生成随机数的线程都有一个不同的生成器,但是它们都是从同一个类中管理的,对程序员来说是透明的。使用此机制,您将获得比使用共享Random
对象生成所有线程的随机数更好的性能。
在本教程中,您将学习如何使用ThreadLocalRandom
类在并发应用中生成随机数。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
TaskLocalRandom
的类,并指定它实现Runnable
接口。public class TaskLocalRandom implements Runnable {
-
实现类的构造函数。使用它使用
current()
方法将随机数生成器初始化为实际线程。public TaskLocalRandom() { ThreadLocalRandom.current(); }
-
执行
run()
方法。获取执行此任务的线程的名称,并使用nextInt()
方法将 10 个随机整数写入控制台。@Override public void run() { String name=Thread.currentThread().getName(); for (int i=0; i<10; i++){ System.out.printf("%s: %d\n",name,ThreadLocalRandom.current().nextInt(10)); } }
-
通过创建名为
Main
的类来实现示例的主类,并向其添加main()
方法。public class Main { public static void main(String[] args) {
-
为三个
Thread
对象创建一个数组。Thread threads[]=new Thread[3];
-
创建并启动三个
TaskLocalRandom
任务。将线程存储在先前创建的数组中。for (int i=0; i<3; i++) { TaskLocalRandom task=new TaskLocalRandom(); threads[i]=new Thread(task); threads[i].start(); }
本例的键在TaskLocalRandom
类中。在类的构造函数中,我们调用了ThreadLocalRandom
类的current()
方法。这是一个静态方法,返回与当前线程关联的ThreadLocalRandom
对象,因此您可以使用该对象生成随机数。如果发出调用的线程尚未关联任何对象,则该类将创建一个新对象。在本例中,使用此方法初始化与此任务关联的随机生成器,以便在下次调用该方法时创建它。
在TaskLocalRandom
类的run()
方法中,调用current()
方法获取与该线程关联的随机生成器,同时调用nextInt()
方法,将数字 10 作为参数传递。此方法将返回一个介于 0 和 10 之间的伪随机数。每个任务生成 10 个随机数。
ThreadLocalRandom
类还提供了生成long
、float
和double
数字以及Boolean
值的方法。有些方法允许您提供一个数字作为参数,以生成介于零和该数字之间的随机数。其他方法允许您提供两个参数以在这些数字之间生成随机数。
- 第一章线程管理中的使用局部线程变量配方
在 Java 版本 5 中引入了原子变量,以提供对单个变量的原子操作。当您使用一个普通变量时,您在 Java 中实现的每个操作都会在编译程序时机器可以理解的几个指令中进行转换。例如,当您为一个变量赋值时,您只使用 Java 中的一条指令,但当您编译此程序时,此指令将转换为 JVM 语言中的各种指令。当您使用共享一个变量的多个线程时,这一事实可能会导致数据不一致错误。
为了避免这些问题,Java 引入了原子变量。当线程使用原子变量执行操作时,如果其他线程希望使用相同的变量执行操作,则类的实现包括一种机制,用于检查操作是否在一个步骤中完成。基本上,该操作获取变量的值,更改局部变量中的值,然后尝试将旧值更改为新值。如果旧值仍然相同,则会进行更改。否则,该方法将再次开始该操作。此操作称为比较设置。
原子变量不使用锁或其他同步机制来保护对其值的访问。它们的所有操作都基于比较和设置操作。它保证了多个线程可以同时处理一个原子变量,而不会产生数据不一致性错误,并且其性能优于使用受同步机制保护的普通变量。
在本食谱中,您将学习如何使用原子变量实现银行帐户和两个不同的任务,一个是向帐户中添加钱,另一个是从帐户中减去钱。您将在示例的实现中使用AtomicLong
类。
此配方的示例已使用 EclipseIDE 实现。如果您正在使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Account
的类来模拟银行账户。public class Account {
-
声明一个名为
balance
的私有AtomicLong
属性来存储帐户余额。private AtomicLong balance;
-
实现类的构造函数以初始化其属性。
public Account(){ balance=new AtomicLong(); }
-
实现名为
getBalance()
的方法返回余额属性的值。public long getBalance() { return balance.get(); }
-
实现名为
setBalance(``)
的方法来建立余额属性的值。public void setBalance(long balance) { this.balance.set(balance); }
-
实现一个名为
addAmount()
的方法来增加balance
属性的值。public void addAmount(long amount) { this.balance.getAndAdd(amount); }
-
实现一个名为
substractAmount()
的方法来减少balance
属性的值。public void subtractAmount(long amount) { this.balance.getAndAdd(-amount); }
-
创建一个名为
Company
的类,并指定它实现Runnable
接口。此类将模拟公司支付的款项。public class Company implements Runnable {
-
声明一个名为
account
的私有Account
属性。private Account account;
-
实现类的构造函数以初始化其属性。
```java
public Company(Account account) {
this.account=account;
}
```
- 执行任务的
run()
方法。使用账户的addAmount()
方法在其余额中增加 10 次 1000。
```java
@Override
public void run() {
for (int i=0; i<10; i++){
account.addAmount(1000);
}
}
```
- 创建一个名为
Bank
的类,并指定它实现Runnable
接口。这个类将模拟从帐户中取款。
```java
public class Bank implements Runnable {
```
- 声明名为
account
的私有Account
属性。
```java
private Account account;
```
- 实现类的构造函数以初始化其属性。
```java
public Bank(Account account) {
this.account=account;
}
```
- 执行任务的
run()
方法。使用账户的subtractAmount()
方法在其余额中减去 10 次 1000。
```java
@Override
public void run() {
for (int i=0; i<10; i++){
account.subtractAmount(1000);
}
}
```
- 通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。
```java
public class Main {
public static void main(String[] args) {
```
- 创建一个
Account
对象,并将其平衡设置为1000
。
```java
Account account=new Account();
account.setBalance(1000);
```
- 创建一个新的
Company
任务和一个线程来执行它。
```java
Company company=new Company(account);
Thread companyThread=new Thread(company);
Create a new Bank task and a thread to execute it.
Bank bank=new Bank(account);
Thread bankThread=new Thread(bank);
```
- 在控制台中写入帐户的初始余额。
```java
System.out.printf("Account : Initial Balance: %d\n",account.getBalance());
```
- 启动线程。
```java
companyThread.start();
bankThread.start();
```
- 使用
join()
方法等待线程完成,并在控制台中写入帐户的最终余额。
```java
try {
companyThread.join();
bankThread.join();
System.out.printf("Account : Final Balance: %d\n",account.getBalance());
} catch (InterruptedException e) {
e.printStackTrace();
}
```
本例的键在Account
类中。在该类中,我们声明了一个名为balance
的AtomicLong
变量来存储帐户余额,然后我们使用AtomicLong
类提供的方法实现了处理该余额的方法。为了实现返回balance
属性值的getBalance()
方法,您使用了AtomicLong
类的get()
方法。为了实现建立 balance 属性值的setBalance()
方法,您使用了AtomicLong
类的set()
方法。为了实现将导入添加到帐户余额的addAmount()
方法,您使用了AtomicLong
类的getAndAdd()
方法,该方法返回值并按指定为参数的值递增。最后,为了实现递减balance
属性值的subtractAmount()
方法,您还使用了getAndAdd()
方法。
然后,您实现了两个不同的任务:
Company
类模拟增加账户余额的公司。该类的每个任务以 1000 为单位递增 10 次。Bank
类模拟一家银行,银行账户的所有权将其资金取出。这门课的每项任务减量 10 次,减量为 1000。
在Main
类中,您创建了一个余额为 1000 的Account
对象。然后,您执行了银行任务和公司任务,因此帐户的最终余额必须与初始余额相同。
当您执行程序时,您将看到最终余额如何与初始余额相同。以下屏幕截图显示了此示例的执行输出:
正如我们在引言中提到的,Java 中还有其他原子类。AtomicBoolean
、AtomicInteger
和AtomicReference
是原子类的其他示例。
- 第 2 章基本线程同步中的同步方法配方
当您实现一个并发应用,其中一个或多个对象由多个线程共享时,您必须使用同步机制作为锁或synchronized
关键字来保护对其属性的访问,以避免数据不一致错误。
这些机制存在以下问题:
- 死锁:当一个线程被阻塞,等待一个被其他线程锁定的锁,并且永远不会释放它时,就会发生这种情况。这种情况会阻塞程序,因此它永远不会结束。
- 如果只有一个线程正在访问共享对象,那么它必须执行获取和释放锁所需的代码。
为了在这种情况下提供更好的性能,开发了比较和交换操作。此操作通过以下三个步骤实现对变量值的修改:
- 您将获得变量的值,即变量的旧值。
- 在时间变量中更改变量的值,即变量的新值。
- 如果旧值等于变量的实际值,则用新值替换旧值。如果另一个线程更改了变量的值,则旧值可能与实际值不同。
使用这种机制,您不需要使用任何同步机制,因此可以避免死锁并获得更好的性能。
Java 在原子变量中实现了这种机制。这些变量提供了compareAndSet()
方法,该方法是比较和交换操作的实现,以及基于它的其他方法。
Java 还引入了原子数组,为integer
或long
号数组提供原子操作。在本食谱中,您将学习如何使用AtomicIntegerArray
类处理原子阵列。
此配方的示例已使用 EclipseIDE 实现。如果您使用 Eclipse 或其他 IDE(如 NetBeans),请打开它并创建一个新的 Java 项目。
按照以下步骤来实现该示例:
-
创建一个名为
Incrementer
的类,并指定它实现Runnable
接口。public class Incrementer implements Runnable {
-
声明一个名为
vector
的私有AtomicIntegerArray
属性来存储一个integer
数字数组。private AtomicIntegerArray vector;
-
实现类的构造函数来初始化其属性。
public Incrementer(AtomicIntegerArray vector) { this.vector=vector; }
-
执行
run()
方法。使用getAndIncrement()
方法增加阵列的所有元素。@Override public void run() { for (int i=0; i<vector.length(); i++){ vector.getAndIncrement(i); } }
-
创建一个名为
Decrementer
的类,并指定它实现Runnable
接口。public class Decrementer implements Runnable {
-
声明一个名为
vector
的私有AtomicIntegerArray
属性来存储一个integer
数字数组。private AtomicIntegerArray vector;
-
实现类的构造函数以初始化其属性。
public Decrementer(AtomicIntegerArray vector) { this.vector=vector; }
-
执行
run()
方法。使用getAndDecrement()
方法递减数组中的所有元素。@Override public void run() { for (int i=0; i<vector.length(); i++) { vector.getAndDecrement(i); } }
-
通过创建一个名为
Main
的类并向其添加main()
方法来实现示例的主类。public class Main { public static void main(String[] args) {
-
声明一个名为
THREADS
的常量,并为其赋值100
。创建一个包含 1000 个元素的AtomicIntegerArray
对象。
```java
final int THREADS=100;
AtomicIntegerArray vector=new AtomicIntegerArray(1000);
```
- 创建一个
Incrementer
任务来处理前面创建的原子数组。
```java
Incrementer incrementer=new Incrementer(vector);
```
- 创建一个
Decrementer
任务来处理前面创建的原子数组。
```java
Decrementer decrementer=new Decrementer(vector);
```
- 创建两个数组来存储 100 个线程对象。
```java
Thread threadIncrementer[]=new Thread[THREADS];
Thread threadDecrementer[]=new Thread[THREADS];
```
- 创建并启动 100 个线程来执行
Incrementer
任务,另外 100 个线程来执行Decrementer
任务。将线程存储在先前创建的数组中。
```java
for (int i=0; i<THREADS; i++) {
threadIncrementer[i]=new Thread(incrementer);
threadDecrementer[i]=new Thread(decrementer);
threadIncrementer[i].start();
threadDecrementer[i].start();
}
```
- 使用
join()
方法等待线程的终结。
```java
for (int i=0; i<100; i++) {
try {
threadIncrementer[i].join();
threadDecrementer[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
- 在控制台中写入原子数组中不同于零的元素。使用
get()
方法获得原子阵列的元素。
```java
for (int i=0; i<vector.length(); i++) {
if (vector.get(i)!=0) {
System.out.println("Vector["+i+"] : "+vector.get(i));
}
}
```
- 在控制台中编写一条消息,指示示例已完成。
```java
System.out.println("Main: End of the example");
```
在本例中,您已经实现了两个不同的任务来处理AtomicIntegerArray
对象:
Incrementer
任务:此类使用getAndIncrement()
方法递增数组中的所有元素Decrementer
任务:这个类使用getAndDecrement()
方法递减数组中的所有元素
在Main
类中,您创建了包含 1000 个元素的AtomicIntegerArray
,然后执行了 100 个递增和 100 个递减任务。在这些任务结束时,如果没有不一致性错误,则数组的所有元素都必须具有值0
。如果执行该程序,您将看到该程序如何仅向控制台写入最终消息,因为所有元素都为零。
现在,Java 只提供了另一个原子数组类。是AtomicLongArray
类提供了与IntegerAtomicArray
类相同的方法。
这些类提供的其他有趣方法包括:
get(int``i)
:返回参数指定的数组位置值set(int``I,``int``newValue)
:建立参数指定的数组位置值。
- 第 6 章并发集合中的使用原子变量配方