Skip to content

Files

Latest commit

c4e46db · Oct 12, 2021

History

History
1158 lines (775 loc) · 43.6 KB

File metadata and controls

1158 lines (775 loc) · 43.6 KB

十、并发处理

在本章中,我们将介绍以下内容:

  • 在 Java7 中使用 join/fork 框架
  • 使用可重复使用的同步栅移相器
  • 在多线程中安全地使用 ConcurrentLinkedQue 类
  • 使用 LinkedTransferQueue 类
  • 使用 ThreadLocalRandom 类支持多线程

导言

Java 7 改进了对并发应用程序的支持。引入了几个支持任务并行执行的新类。 ForkJoinPool类用于使用分治技术解决问题的应用程序。每个子问题都作为一个单独的线程分叉(拆分),如果需要提供解决方案,随后再连接。此类使用的线程通常是 java.util.concurrent.ForkJoinTask类的子类,是轻量级线程。Java 配方中的使用连接/分叉框架说明了这种方法的使用。

此外,还引入了 java.util.concurrent.Phaser类来支持在一系列阶段执行线程集合。一组线程是同步的,因此它们都执行,然后等待其他线程完成。一旦它们全部完成,就可以在第二阶段或后续阶段重新执行它们。使用可重复使用的同步屏障移相器配方的说明了在游戏引擎设置中使用此类。

使用 java.util.concurrent.concurrentLinkedQueue 类与多线程安全配合使用使用 java.util.concurrent.LinkedTransferQueue 类配方引入了两个新类,旨在与多线程安全配合使用。举例说明了它们在支持生产者/消费者框架中的应用。

java.util.concurrent.ThreadLocalRandom类是新的,它为多线程之间使用的随机数生成提供了更好的支持。在使用 ThreadLocalRandom 类配方支持多线程的中讨论了这一点。

java.util.ConcurrentModificationException类中添加了两个新构造函数。它们都接受用于指定异常原因的 Throwable对象。其中一个构造函数还接受一个字符串,该字符串提供有关异常的详细消息。

Java7 通过修改锁定机制来避免死锁,从而改进了类装入器的使用。在 Java7 之前的多线程自定义类加载器中,某些自定义类加载器在使用循环委托模型时容易出现死锁。

考虑下面的场景。Thread1 尝试使用 ClassLoader1(锁定 ClassLoader1)加载 class1。然后将 class2 的加载委托给 ClassLoader2。同时,Thread2 使用 ClassLoader2(锁定 ClassLoader2)加载 class3,然后将 class4 的加载委托给 ClassLoader1。由于两个类加载器都被锁定,并且两个线程都需要两个加载器,因此会发生死锁情况。

并发类加载器的理想行为是从同一个类加载器实例并发加载不同的类。这需要在更精细的粒度级别上进行锁定,例如根据要加载的类的名称锁定类加载器。

不应在类装入器级别执行同步。相反,应该在类级别上进行锁定,其中类加载器一次只允许该类加载器加载该类的单个实例。

有些类装入器能够同时装入类。这种类型的类加载器称为具有并行能力的类加载器。他们需要在初始化过程中使用 registerAsParallelCapable方法进行注册。

如果自定义类加载器使用非循环的分层委托模型,那么 Java 中不需要任何更改。在分层委托模型中,委托首先被委托给其父类加载器。在 Java 中,不使用分层委托模型的类加载器应构造为具有并行能力的类加载器。

要避免自定义类装入器的死锁,请执行以下操作:

  • 在类初始化序列中使用 registerAsParallelCapable方法。这表明类装入器的所有实例都是多线程安全的。

  • 确保类加载器代码是多线程安全的。这涉及到:

    • 使用内部锁定方案,例如 java.lang.ClassLoader
    • 使用的类名锁定方案删除类加载器锁
    • 上的任何同步,确保关键部分是多线程安全的
  • 建议类加载器重写 findClass(String)方法

  • 如果重写了 defineClass方法,则确保每个类名只调用它们一次

有关此问题的更多详细信息,请参见http://openjdk.java.net/groups/core-libs/ClassLoaderProposal.html

在 Java 中使用 join/fork 框架

连接/分叉框架是一种支持将问题分解成越来越小的部分,并行解决,然后组合结果的方法。新的 java.util.concurrent.ForkJoinPool类支持这种方法。它设计用于多核系统,理想情况下可与数十或数百个处理器协同工作。目前,很少有桌面平台支持这种类型的并发,但未来的机器将支持这种类型的并发。如果处理器少于四个,则性能几乎不会提高。

ForkJoinPool类是从 java.util.concurrent.AbstractExecutorService派生出来的,使其成为 ExecutorService。它设计用于与 ForkJoinTasks配合使用,但也可与普通螺纹配合使用。 ForkJoinPool类与其他执行器的不同之处在于,它的线程尝试查找并执行由其他当前正在运行的任务创建的子任务。这被称为工作盗窃

ForkJoinPool类可用于子问题的计算被修改或返回值的问题。返回值时,将使用一个 java.util.concurrent.RecursiveTask派生类。否则,使用 java.util.concurrent.RecursiveAction类。在这个配方中,我们将演示 RecursiveTask派生类的使用。

准备好了吗

要将 fork/join 框架用于为每个子任务返回结果的任务:

  1. 创建一个子类 RecursiveTask,实现所需的计算。
  2. 创建 ForkJoinPool类的实例。
  3. RecursiveTask类的子类实例使用 ForkJoinPool类的 invoke方法。

怎么做。。。

此应用程序并不打算以最有效的方式实现,而是用于说明 fork/join 任务。因此,在处理器数量较少的系统上,性能改进可能很少或根本没有。

  1. 创建一个新的控制台应用程序。我们将使用从 RecursiveTask派生的静态内部类来计算 numbers数组中整数的平方和。首先,声明 numbers数组如下:

    private static int numbers[] = new int[100000];
  2. 添加 SumOfSquaresTask类,如下所示。它创建数组元素的子范围,并使用迭代循环计算它们的平方和,或者根据阈值大小将数组分成更小的部分:

    private static class SumOfSquaresTask extends RecursiveTask<Long> {
    private final int thresholdTHRESHOLD = 1000;
    private int from;
    private int to;
    public SumOfSquaresTask(int from, int to) {
    this.from = from;
    this.to = to;
    }
    @Override
    protected Long compute() {
    long sum = 0L;
    int mid = (to + from) >>> 1;
    if ((to - from) < thresholdTHRESHOLD) {
    for (int i = from; i < to; i++) {
    sum += numbers[i] * numbers[i];
    }
    return sum;
    }
    else {
    List<RecursiveTask<Long>> forks = new ArrayList<>();
    SumOfSquaresTask task1 =
    new SumOfSquaresTask(from, mid);
    SumOfSquaresTask task2 =
    new SumOfSquaresTask(mid, to);
    forks.add(task1);
    task1.fork();
    forks.add(task2);
    task2.fork();
    for (RecursiveTask<Long> task : forks) {
    sum += task.join();
    }
    return sum;
    }
    }
    }
  3. 添加以下 main方法。出于比较目的,平方和使用 For 循环计算,然后使用 ForkJoinPool类。执行时间的计算和显示如下:

    public static void main(String[] args) {
    for (int i = 0; i < numbers.length; i++) {
    numbers[i] = i;
    }
    long startTime;
    long stopTime;
    long sum = 0L;
    startTime = System.currentTimeMillis();
    for (int i = 0; i < numbers.length; i++) {
    sum += numbers[i] * numbers[i];
    }
    System.out.println("Sum of squares: " + sum);
    stopTime = System.currentTimeMillis();
    System.out.println("Iterative solution time: " + (stopTime - startTime));
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    startTime = System.currentTimeMillis();
    long result = forkJoinPool.invoke(new SumOfSquaresTask(0, numbers.length));
    System.out.println("forkJoinPool: " + forkJoinPool.toString());
    stopTime = System.currentTimeMillis();
    System.out.println("Sum of squares: " + result);
    System.out.println("Fork/join solution time: " + (stopTime - startTime));
    }
  4. Execute the application. Your output should be similar to the following. However, you should observe different execution times depending on your hardware configuration:

    平方和:18103503627376

    迭代求解时间:5

    平方和:18103503627376

    分叉/连接解决时间:23

请注意,迭代解决方案比使用 fork/join 策略的解决方案要快。如前所述,除非有大量处理器,否则这种方法并不总是更有效。

重复运行应用程序将导致不同的结果。更积极的测试方法是在可能不同的处理器加载条件下重复执行解决方案,然后取结果的平均值。阈值的大小也会影响其性能。

它是如何工作的。。。

numbers数组已声明为 100000 个元素的整数数组。 SumOfSquaresTask类是使用泛型类型 LongRecursiveTask类派生而来的。设置了 1000 个阈值。任何小于此阈值的子阵列都使用迭代求解。否则,将该部分分成两半,并创建两个新任务,每一半一个。

ArrayList用于保存这两个子任务。这是严格不需要的,实际上会减慢计算速度。但是,如果我们决定将数组划分为两个以上的段,这将非常有用。它提供了一种在子任务连接时重新组合元素的方便方法。

使用 fork方法分割子任务。它们进入线程池,最终将被执行。 join方法在子任务完成时返回结果。将子任务的总和相加,然后返回。

main方法中,第一个代码段使用 for循环计算平方和。启动和停止时间基于以毫秒为单位测量的当前时间。第二段创建了一个 ForkJoinPool类的实例,然后将其 invoke方法用于 SumOfSquaresTask对象的一个新实例。传递给 SumOfSquaresTask构造函数的参数指示它从数组的第一个元素开始,以最后一个元素结束。完成后,将显示执行时间。

还有更多。。。

ForkJoinPool类有几种报告池状态的方法,包括:

  • getPoolSize:此方法返回已启动但未完成的线程数
  • getRunningThreadCount:此方法返回未阻塞但正在等待加入其他任务的线程数的估计值
  • getActiveThreadCount:此方法返回执行任务的线程数估计值

ForkJoinPool类的 toString方法返回池的几个方面。在 invoke方法执行后立即添加以下语句:

out.println("forkJoinPool: " + forkJoinPool);

当程序执行时,您将获得类似于以下内容的输出:

forkJoinPool:java.util.concurrent。ForkJoinPool@18fb53f6[正在运行,并行度=4,大小=55,活动=0,运行=0,窃取=171,任务=0,提交=0]

另见

使用可重用的同步屏障移相器配方为执行多个线程提供了不同的方法。

使用可重复使用的同步栅相器

java.util.concurrent.Phaser类涉及在循环类型阶段一起工作的线程的同步。线程将执行,然后等待组中其他线程的完成。当所有线程都完成时,一个阶段完成。然后可以使用 Phaser再次协调同一组线程的执行。

java.util.concurrent.CountdownLatch类提供了一种方法,但需要固定数量的线程,并且默认执行一次。在 Java5 中引入的 java.util.concurrent.CyclicBarrier也使用了固定数量的线程,但可以重用。但是,不可能进入下一阶段。当一个问题的特征是一系列步骤/阶段,这些步骤/阶段根据某些标准从一个阶段推进到下一个阶段时,这非常有用。

随着 Java 7 中 Phaser类的引入,我们现在有了一个并发抽象,它结合了 CountDownLatchCyclicBarrier的特性,并增加了对动态线程数的支持。术语“阶段”指的是线程可以协调以在不同的阶段或步骤中执行的想法。所有线程都将执行,然后等待其他线程完成。一旦完成,他们将重新开始并完成第二个或后续操作阶段。

屏障是一种阻止任务继续进行直到满足某些条件的块。一种常见情况是所有相关线程都已完成。

Phaser类提供了几个特性,这使得它非常有用:

  • 可以动态地从线程池中添加和删除参与方
  • 每个相位都有一个唯一的相位号
  • Phaser可以终止,导致任何等待的线程立即返回
  • 发生的异常不会影响屏障的状态

register方法增加参与方的数量。当内部计数达到零或由某些其他标准集确定时,相位器终止。

准备好了吗

我们将开发一个模拟游戏引擎操作的应用程序。第一个版本将创建一系列代表游戏参与者的任务。我们将使用 Phaser类来协调它们的交互。

使用 Phaser类同步一组任务的开始:

  1. 创建将参与相位器的 Runnable对象集合。

  2. 创建 Phaser类的实例。

  3. 对于每个参与者:

    • 注册参与者
    • 使用参与者的 Runnable对象
    • 创建一个新线程,使用 arriveAndAwaitAdvance方法等待创建其他任务
    • 执行线程
  4. 使用 Phaser对象的 arriveAndDeregister启动参与者的执行。

怎么做。。。

  1. 使用名为 GamePhaserExample的类创建新的控制台应用程序。我们将创建一个表示游戏参与者的内部类的简单层次结构。添加 Entity类作为基础抽象类,定义如下。虽然不是绝对必要,但我们将使用继承来简化这些类型应用程序的开发:

    private static abstract class Entity implements Runnable {
    public abstract void run();
    }
  2. 接下来,我们将创建两个派生类: PlayerZombie。这些类实现了 run方法和 toString方法。 run方法使用 sleep方法模拟所执行的工作。不出所料,僵尸比人类慢:

    private static class Player extends Entity {
    private final static AtomicInteger idSource = new AtomicInteger();
    private final int id = idSource.incrementAndGet();
    public void run() {
    System.out.println(toString() + " started");
    try {
    Thread.currentThread().sleep(
    ThreadLocalRandom.current().nextInt(200, 600));
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    System.out.println(toString() + " stopped");
    }
    @Override
    public String toString() {
    return "Player #" + id;
    }
    }
    private static class Zombie extends Entity {
    private final static AtomicInteger idSource = new AtomicInteger();
    private final int id = idSource.incrementAndGet();
    public void run() {
    System.out.println(toString() + " started");
    try {
    Thread.currentThread().sleep(
    ThreadLocalRandom.current().nextInt(400, 800));
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    System.out.println(toString() + " stopped");
    }
    @Override
    public String toString() {
    return "Zombie #" + id;
    }
    }
  3. 为了使示例更清晰,在 GamePhaserExample类中添加以下 mainmethodoid:

    public static void main(String[] args) {
    new GamePhaserExample().execute();
    }
  4. 接下来,添加以下 execute方法,我们在其中创建参与者列表,然后调用 gameEngine方法:

    private void execute() {
    List<Entity> entities = new ArrayList<>();
    entities = new ArrayList<>();
    entities.add(new Player());
    entities.add(new Zombie());
    entities.add(new Zombie());
    entities.add(new Zombie());
    gameEngine(entities);
    }
  5. gameEngine方法如下。 for each循环为每个参与者创建一个线程:

    private void gameEngine(List<Entity> entities) {
    final Phaser phaser = new Phaser(1);
    for (final Entity entity : entities) {
    synchronization barrier Phaserusingfinal String member = entity.toString();
    System.out.println(member + " joined the game");
    phaser.register();
    new Thread() {
    @Override
    public void run() {
    System.out.println(member +
    " waiting for the remaining participants");
    phaser.arriveAndAwaitAdvance(); // wait for remaining entities
    System.out.println(member + " starting run");
    entity.run();
    }
    }.start();
    }
    phaser.arriveAndDeregister(); //Deregister and continue
    System.out.println("Phaser continuing");
    }
  6. Execute the application. The output is non-deterministic, but should be similar to the following:

    玩家#1 加入游戏

    僵尸 1 加入游戏

    僵尸 2 加入游戏

    玩家#1 等待剩余参与者

    僵尸#1 等待剩余参与者

    僵尸 3 加入游戏

    移相器继续

    僵尸#3 等待剩余参与者

    僵尸 2 等待剩余参与者

    僵尸#1 开始跑步

    僵尸#1 启动

    僵尸#3 开始跑步

    僵尸 3 开始

    僵尸#2 开始跑步

    僵尸 2 启动

    选手#1 起跑

    玩家#1 开始

    玩家#1 停止

    僵尸#1 停止

    僵尸#3 停止

    僵尸 2 停止

注意, Phaser对象等待所有参与者加入游戏。

它是如何工作的。。。

sleep方法用于模拟该实体所涉及的工作。注意 ThreadLocalRandom类的使用。它的 nextInt方法返回了一个随机数,介于其参数中指定的值之间。当使用并发线程时,这是生成随机数的首选方法,如使用 ThreadLocalRandom 类配方支持多线程的中所述。

AtomicInteger类的一个实例用于为创建的每个对象分配唯一的 ID。这是一种在线程中生成数字的安全方法。 toString方法返回实体的简单字符串表示形式。

execute方法中,我们创建了一个 ArrayList来容纳参与者。注意在创建 ArrayList时使用了菱形操作符。在第 1 章Java 语言改进中的使用菱形运算符进行构造函数类型推断配方中解释了此 Java 7 语言改进。增加了一名玩家和三名僵尸。僵尸的数量似乎总是超过人类。然后调用了 gameEngine方法。

创建了一个 Phaser对象,参数为 1,表示第一个参与者。它不是一个实体,只是作为一种机制来帮助控制移相器。

在 for each 循环中,使用 register方法将相位器中的参与方数量增加 1。使用匿名内部类创建了一个新线程。在其 run方法中,实体直到所有参与者到达后才启动。 arriveAndAwaitAdvance方法导致通知参与者已到达,并且在所有参与者到达且阶段完成之前,该方法不应返回。

while循环的每次迭代开始时,注册参与者的数量比已经到达的参与者的数量多一个。 register方法将该内部计数增加 1。内部计数比到达的数字多出两个。当执行 arriveAndAwaitAdvance方法时,现在等待的参与者数量将比已经注册的参与者多一个。

循环终止后,仍有一个注册方比到达的参与者多。然而,当 arriveAndDeregister方法执行时,到达的参与者数量的内部计数与参与者数量匹配,线程开始。此外,登记缔约方的数目减少了一个。当所有线程终止时,应用程序终止。

还有更多。。。

可以使用 bulkRegister方法注册一组当事人。此方法采用单个整数参数,指定要注册的参与方数。

在某些情况下,可能需要强制终止相位器。 forceTermination方法用于此目的。

在移相器执行期间,有几种方法将返回有关移相器状态的信息,详见下表。如果相位器已终止,则这些方法将无效:

|

方法

|

描述

| | --- | --- | | getRoot | 返回根移相器。与相位器树一起使用 | | getParent | 返回移相器的父级 | | getPhase | 返回当前阶段号 | | getArrivedParties | 已达到本阶段的缔约方数量 | | getRegisteredParties | 注册方的数量 | | getUnarrivedParties | 尚未达到这一阶段的缔约方数目 |

可以构建相位器树,其中相位器被创建为任务的一个分支。 getRoot方法在这种情况下很有用。相位器结构在中讨论 http://www.cs.rice.edu/~vs3/PDF/SPSS08-phasers.PDF

使用移相器重复一系列任务

我们还可以使用 Phaser类来支持一系列阶段,在这些阶段中执行任务,执行可能的中间操作,然后再次重复一系列任务。

为了支持此行为,我们将修改 gameEngine方法。修改内容包括:

  • 添加一个 iterations变量
  • 重写 Phaser类的 onAdvance方法
  • 在由 isTerminated方法控制的每个任务的 run方法中使用 while循环

添加一个名为 iterations的变量,并将其初始化为 3。这用于指定我们将使用多少阶段。另外,覆盖如下所示的 onAdvance方法:

final int iterations = 3;
final Phaser phaser = new Phaser(1) {
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("Phase number " + phase + " completed\n")
return phase >= iterations-1 || registeredParties == 0;
}
};

每个阶段都是唯一编号的,从零开始。对 onAdvance的调用传递当前相位号和注册到相位器的当前参与方数。当注册方的数量变为零时,此方法的默认实现返回 true。这导致相位器被终止。

此方法的实现导致仅当相位号超过 iterations值(即-1)或没有使用相位器的注册方时,该方法才会返回 true

修改 run方法,如下代码所示:

for (final Entity entity : entities) {
final String member = entity.toString();
System.out.println(member + " joined the game");
phaser.register();
new Thread() {
@Override
public void run() {
do {
System.out.println(member + " starting run");
entity.run();
System.out.println(member +
" waiting for the remaining participants during phase " +
phaser.getPhase());
phaser.arriveAndAwaitAdvance(); // wait for remaining entities
}
while (!phaser.isTerminated());
}
}.start();
}

允许实体先运行,然后等待其他参与者完成并到达。只要相位器没有按照 isTerminated方法确定终止,当所有人都准备好时,将执行下一阶段。

最后一步是使用 arriveAndAwaitAdvance方法将移相器提前到下一阶段。同样,只要移相器没有终止,当每个参与者到达时,移相器将进入下一个阶段。使用以下代码序列完成此操作:

while (!phaser.isTerminated()) {
phaser.arriveAndAwaitAdvance();
}
System.out.println("Phaser continuing");

只使用一个玩家和一个僵尸执行程序。这将减少输出量,并应类似于以下内容:

玩家#1 加入游戏

僵尸 1 加入游戏

选手#1 起跑

玩家#1 开始

僵尸#1 开始跑步

僵尸#1 启动

玩家#1 停止

玩家#1 在第 0 阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第 0 阶段等待剩余参与者

第 0 阶段完成

选手#1 起跑

玩家#1 开始

僵尸#1 开始跑步

僵尸#1 启动

玩家#1 停止

玩家#1 在第一阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第一阶段等待剩余的参与者

第 1 阶段已完成

僵尸#1 开始跑步

选手#1 起跑

僵尸#1 启动

玩家#1 开始

玩家#1 停止

玩家#1 在第二阶段等待剩余参与者

僵尸#1 停止

僵尸#1 在第二阶段等待剩余参与者

第二阶段已完成

移相器继续

另见

有关为多个线程生成随机数的更多信息,请参见使用与当前线程隔离的随机数生成器配方。

通过多线程安全地使用新的 ConcurrentLinkedQue

java.util.concurrent.ConcurrentLinkedDeque类是 Java Collections 框架的一个成员,它为多个线程同时安全地访问同一个数据集合提供了能力。该类实现了一个双端队列,称为deque,并允许从 deque 的两端插入和移除元素。它也称为头尾链表,与其他并发集合一样,不允许使用 null 元素。

在此配方中,我们将演示 ConcurrentLinkedDeque类的基本实现,并说明一些最常用方法的使用。

准备好了吗

要在生产者/消费者框架中使用 ConcurrentLinkedDeque

  1. 创建一个 ConcurrentLinkedDeque的实例。
  2. 定义要放置到三角形中的图元。
  3. 实现生产者线程以生成要放置在 deque 中的元素。
  4. 实现一个使用者线程以从 deque 中删除元素。

怎么做。。。

  1. 创建一个新的控制台应用程序。使用泛型类型 Item声明 ConcurrentLinkedDeque的私有静态实例。 Item类被声明为内部类。包括 get 方法和构造函数,如下代码所示,使用两个属性, descriptionitemId:

    private static ConcurrentLinkedDeque<Item> deque = new ConcurrentLinkedDeque<>();
    static class Item {
    privateublic final String description;
    privateublic final int itemId;
    public Item() {
    "this(Default Item";, 0)
    }
    public Item(String description, int itemId) {
    this.description = description;
    this.itemId = itemId;
    }
    }
  2. 然后创建一个 producer 类来生成类型为 Item的元素。出于这个配方的目的,我们只需要生成七个项目,然后打印一条语句来证明该项目已添加到 deque 中。我们使用 ConcurrentLinkedDeque类的 add方法添加元素。每次添加后,线程短暂休眠:

    static class ItemProducer implements Runnable {
    @Override
    public void run() {
    String itemName = "";
    int itemId = 0;
    try {
    for (int x = 1; x < 8; x++) {
    itemName = "Item" + x;
    itemId = x;
    deque.add(new Item(itemName, itemId));
    System.out.println("New Item Added:" + itemName + " " + itemId);
    Thread.currentThread().sleep(250);
    }
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    }
    }
  3. 接下来,创建一个 consumer 类。为了确保在使用者线程尝试访问 deque 时,deque 中将包含元素,我们在检索元素之前让线程休眠一秒钟。然后我们使用 pollFirst方法检索 deque 中的第一个元素。如果元素不为 null,那么我们将元素传递给 generateOrder方法。在这种方法中,我们打印出关于项目的信息:

    static class ItemConsumer implements Runnable {
    @Override
    public void run() {
    try {
    Thread.currentThread().sleep(1000);
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    Item item;
    while ((item = deque.pollFirst()) != null) {
    {
    generateOrder(item);
    }
    }
    private void generateOrder(Item item) {
    System.out.println("Part Order");
    System.out.println("Item description: " + item.getDescriptiond());
    System.out.println("Item ID # " + item.getItemIdi());
    System.out.println();
    try {
    Thread.currentThread().sleep(1000);
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    }
    }
  4. 最后,在我们的 main方法中,我们启动两个线程:

    public static void main(String[] args) {
    new Thread(new ItemProducer());.start()
    new Thread(new ItemConsumer());.start()
    }
  5. When you execute the program, you should see output similar to the following:

    新增项目:项目 1

    新增项目:项目 2

    新增项目:项目 3

    新增项目:项目 4

    零件订单

    项目说明:项目 1

    项目 ID#1

    新增项目:项目 5

    新增项目:项目 6

    新增项目:项目 7

    零件订单

    项目说明:项目 2

    项目 ID#2

    零件订单

    项目说明:项目 3

    项目 ID#3

    零件订单

    项目说明:项目 4

    项目 ID#4

    零件订单

    项目说明:项目 5

    项目 ID#5

    零件订单

    项目说明:项目 6

    项目 ID#6

    零件订单

    项目说明:项目 7

    项目 ID#7

它是如何工作的。。。

当我们启动这两个线程时,我们给了生产者线程一个开始,用项目填充我们的 deque。一秒钟后,使用者线程开始检索元素。 ConcurrentLinkedDeque类的使用允许两个线程同时安全地访问 deque 的元素。

在我们的示例中,我们使用方法 addpollFirst添加和删除 deque 的元素。有许多方法可用,其中许多基本上以相同的方式运行。还有更多。。。第节提供了有关访问 deque 元素的各种选项的更多详细信息。

还有更多。。。

我们将讨论几个主题,包括:

  • 异步并发线程的问题
  • 向 deque 添加元素
  • 从 deque 中检索元素
  • 访问 deque 的特定元素

异步并发线程的问题

由于在任何给定时刻都可能有多个线程访问集合, size方法并不总是返回准确的结果。使用 iteratordescendingIterator方法时也是如此。此外,任何批量数据操作,如 addAllremoveAll,并不总是能够达到预期的结果。如果一个线程正在访问集合中的某个项目,而另一个线程尝试提取所有项目,则批量操作不能保证以原子方式运行。

有两种 toArray方法可用于检索 deque 的所有元素并将它们存储在一个数组中。第一种方法返回一个表示 deque 所有元素的对象数组,可以转换为适当的数据类型。当 deque 的元素具有不同的数据类型时,这非常有用。下面是如何使用前面的线程示例使用 toArray方法的第一种形式的示例:

Item[] items = (Item[]) deque.toArray();

另一个 toArray方法需要一个特定数据类型的初始化数组作为参数,并返回该数据类型的元素数组。

Item[] items = deque.toArray(new Item[0]);

向 deque 添加元素

下表列出了一些可用于将元素添加到 deque 的方法。下表中分组在一起的方法基本上执行相同的功能。这种类似方法的多样性是 ConcurrentLinkedDeque类实现略有不同的接口的结果:

|

方法名

|

将元素添加到

| | --- | --- | | add(Element e)``offer(Element e)``offerLast(Element e)``addLast(Element e) | 末日 | | addFirst(Element e)``offerFirst(Element e)``push(Element e) | 头巾 |

从 deque 中检索元素

以下是可用于从 deque 检索元素的一些方法:

|

方法名

|

错误动作

|

作用

| | --- | --- | --- | | element() | 如果 deque 为空,则引发异常 | 检索但不删除 deque 的第一个元素 | | getFirst() |   |   | | getLast() |   |   | | peek() | 如果 deque 为空,则返回 null |   | | peekFirst() |   |   | | peekLast() |   |   | | pop() | 如果 deque 为空,则引发异常 | 检索并删除 deque 的第一个元素 | | removeFirst() |   |   | | poll() | 如果 deque 为空,则返回 null |   | | pollFirst() |   |   | | removeLast() | 如果 deque 为空,则引发异常 | 检索并删除 deque 的最后一个元素 | | pollLast() | 如果 deque 为空,则返回 null |   |

访问 deque 的特定元素

以下是可用于访问 deque 特定元素的一些方法:

|

方法名

|

作用

|

评论

| | --- | --- | --- | | contains(Element e) | 如果 deque 至少包含一个等于 Element e的元素,则返回 true |   | | remove(Element e)``removeFirstOccurrence(Element e) | 删除 deque 中第一次出现的等于 Element e的元素 | 如果 deque 中不存在该元素,则 deque 将保持不变。如果 e为空,则引发异常 | | removeLastOccurrence(Element e) | 删除 deque 中最后出现的等于 Element e的元素 |   |

使用新的 LinkedTransferQueue 类

java.util.concurrent.LinkedTransferQueue类实现 java.util.concurrent.TransferQueue接口,是一个无限队列,队列元素遵循先进先出模型。此类提供了用于检索元素的阻塞方法和非阻塞方法,是多线程并发访问的合适选择。在这个配方中,我们将创建一个 LinkedTransferQueue的简单实现,并探索这个类中可用的一些方法。

准备好了吗

在生产者/消费者框架中使用 LinkedTransferQueue

  1. 创建一个 LinkedTransferQueue的实例。
  2. 定义要放入队列的元素类型。
  3. 实现生产者线程以生成要放置在队列中的元素。
  4. 实现使用者线程以从队列中删除元素。

怎么做。。。

  1. 创建一个新的控制台应用程序。使用泛型类型 Item声明 LinkedTransferQueue的私有静态实例。然后创建内部类 Item并包含 get 方法和构造函数,如下代码所示,使用两个属性 descriptionitemId,如下所示:

    private static LinkedTransferQueue<Item>
    linkTransQ = new LinkedTransferQueue<>();
    static class Item {
    public final String description;
    public final int itemId;
    public Item() {
    this("Default Item", 0) ;
    }
    public Item(String description, int itemId) {
    this.description = description;
    this.itemId = itemId;
    }
    }
  2. 接下来,创建一个 producer 类来生成类型为 Item的元素。出于此配方的目的,我们只生成七个项目,然后打印一条语句,以证明该项目已添加到队列中。我们将使用 LinkedTransferQueue类的 offer方法来添加元素。每次添加后,线程都会短暂休眠,我们会打印出所添加项目的名称。然后,我们使用 hasWaitingConsumer方法确定是否有任何消费者线程等待项目可用:

    static class ItemProducer implements Runnable {
    @Override
    public void run() {
    try {
    for (int x = 1; x < 8; x++) {
    String itemName = "Item" + x;
    int itemId = x;
    linkTransQ.offer(new Item(itemName, itemId));
    System.out.println("New Item Added:" + itemName + " " + itemId);
    Thread.currentThread().sleep(250);
    if (linkTransQ.hasWaitingConsumer()) {
    System.out.println("Hurry up!");
    }
    }
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    }
    }
  3. 接下来,创建一个 consumer 类。为了演示 hasWaitingConsumer方法的功能,我们在检索元素之前让线程休眠一秒钟,以确保首先没有等待的消费者。然后,在 while循环中,我们使用 take方法删除列表中的第一项。我们选择了 take方法,因为它是一种阻塞方法,将等待队列具有可用元素。一旦消费者线程能够获取一个元素,我们将该元素传递给 generateOrder方法,该方法将打印出关于该项目的信息:

    static class ItemConsumer implements Runnable {
    @Override
    public void run() {
    try {
    Thread.currentThread().sleep(1000);
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    while (true) {
    try {
    generateOrder(linkTransQ.take());
    }
    catch (InterruptedException ex) {
    ex.printStackTrace();
    }
    }
    }
    private void generateOrder(Item item) {
    System.out.println();
    System.out.println("Part Order");
    System.out.println("Item description: " + item.description());
    System.out.println("Item ID # " + item.itemId());
    }
    }
  4. 最后,在我们的 main方法中,我们启动两个线程:

    public static void main(String[] args) {
    new Thread(new ItemProducer()).start();
    new Thread(new ItemConsumer()).start();
    }
  5. When you execute the program, you should see output similar to the following:

    新增项目:项目 1

    新增项目:项目 2

    新增项目:项目 3

    新增项目:项目 4

    零件订单

    项目说明:项目 1

    项目 ID#1

    零件订单

    项目说明:项目 2

    项目 ID#2

    零件订单

    项目说明:项目 3

    项目 ID#3

    零件订单

    项目说明:项目 4

    项目 ID#4

    快点!

    新增项目:项目 5

    零件订单

    项目说明:项目 5

    项目 ID#5

    快点!

    零件订单

    项目说明:项目 6

    项目 ID#6

    新增项目:项目 6

    快点!

    零件订单

    项目说明:项目 7

    项目 ID#7

    新增项目:项目 7

    快点!

它是如何工作的。。。

当我们启动两个线程时,我们给生产者线程一个开头,通过在 ItemConsumer类中睡眠一秒钟来填充队列中的项目。请注意, hasWaitingConsumer方法最初返回 false,因为消费者线程尚未执行 take方法。一秒钟后,使用者线程开始检索元素。每次检索时, generateOrder方法都会打印出所检索元素的相关信息。在检索完队列中的所有元素后,请注意最后的*快点!*声明,表示仍有消费者在等待。在本例中,由于使用者在 while循环中使用阻塞方法,因此线程永远不会终止。在现实生活中,应该以更优雅的方式终止线程,例如向使用者线程发送终止消息。

在我们的示例中,我们使用方法 offertake添加和删除队列的元素。还有其他可用的方法,这些方法将在中讨论,还有更多。。。部分

还有更多。。。

在这里,我们将讨论以下内容:

  • 异步并发线程的问题
  • 向队列中添加元素
  • 从 deque 中检索元素

异步并发线程的问题

由于在任何给定时刻都可能有多个线程访问集合, size方法并不总是返回准确的结果。此外,任何批量数据操作,例如 addAllremoveAll,并不总是能够达到预期的结果。如果一个线程正在访问集合中的某个项目,而另一个线程尝试提取所有项目,则批量操作不能保证以原子方式运行。

向队列中添加元素

以下是一些可用于向队列添加元素的方法:

|

方法名

|

将元素添加到

|

评论

| | --- | --- | --- | | add(Element e) | 队列结束 | 队列是无界的,因此该方法永远不会返回 false或抛出异常 | | offer(Element e) |   | 队列是无界的,所以方法永远不会返回 false | | put(Element e) |   | 队列是无界的,因此该方法永远不会阻塞 | | offer(Element``e, Long tTimeUnit u) | 队列结束在放弃之前,等待 u 型的 t 时间单位 | 队列是无界的,所以方法总是返回 true |

从 deque 中检索元素

以下是可用于从 deque 检索元素的一些方法:

|

方法名

|

作用

|

评论

| | --- | --- | --- | | peek() | 检索,但不删除队列的第一个元素 | 如果队列为空,则返回 null | | poll() | 删除队列的第一个元素 | 如果队列为空,则返回 null | | poll(Long t, TimeUnit u) | 从队列前面移除元素,在放弃之前等待时间 t(单位为 u) | 如果在元素可用之前时间限制已到,则返回 null | | remove(Object e) | 从队列中删除等于 Object e的元素 | 如果找到并删除了元素,则返回 true | | take() | 删除队列的第一个元素 | 如果阻塞时中断,则引发异常 | | transfer(Element e) | 将元素传输到使用者线程,必要时等待 | 将在队列末尾插入一个元素,并等待使用者线程检索它 | | tryTransfer(Element e) | 将元素立即传输给使用者 | 如果消费者不可用,返回 false | | tryTransfer(Element e, Time t, TimeUnit u) | 立即或在 t(单位 u)指定的时间内将元素传输给使用者 | 如果消费者在超过时间限制后不可用,则返回 false |

使用 ThreadLocalRandom 类支持多线程

java.util.concurrent包有一个新类 ThreadLocalRandom,它支持类似于 Random类的功能。但是,与使用 Random类相比,使用具有多线程的新类将导致更少的争用和更好的性能。当多个线程需要使用随机数时,应使用 ThreadLocalRandom类。随机数生成器位于当前线程的本地。这个食谱检查如何使用这个类。

准备好了吗

建议使用此类的方法是:

  1. 使用静态 current方法返回 ThreadLocalRandom类的实例。
  2. 对该对象使用类的方法。

怎么做。。。

  1. 创建一个新的控制台应用程序。在 main方法中增加以下代码:

    System.out.println("Five random integers");
    for(int i = 0; i<5; i++) {
    System.out.println(ThreadLocalRandom.current(). nextInt());
    }
    System.out.println();
    System.out.println("Random double number between 0.0 and 35.0");
    System.out.println(ThreadLocalRandom.current().nextDouble(35.0));
    System.out.println();
    System.out.println("Five random Long numbers between 1234567 and 7654321");
    for(int i = 0; i<5; i++) {
    System.out.println(
    ThreadLocalRandom.current().nextLong(1234567L, 7654321L));
    }
  2. Execute the program. Your output should appear similar to the following:

    五个随机整数

    0

    4232237

    178803790

    758674372

    1565954732

    0.0 到 35.0 之间的随机双精度数

    3.19657114914888

    1234567 和 7654321之间的五个随机长数

    7525440

    2545475

    1320305

    1240628

    1728476

它是如何工作的。。。

nextInt方法执行五次,并显示其返回值。请注意,该方法最初返回 0。 ThreadLocalRandom类扩展了 Random类。但是,不支持 setSeed方法。如果你尝试使用它,它会抛出一个 UnsupportedOperationException.

然后执行 nextDouble方法。此版本的重载方法返回的数字介于 0.0 和 35.0 之间。 nextLong方法使用两个参数执行了五次,这两个参数指定了其起始(包含)和结束(排除)范围值。

还有更多。。。

此类的方法返回均匀分布的数。下表总结了其方法:

提示

指定范围时,起始值为包含值,结束值为独占值。

|

方法

|

参数

|

退换商品

| | --- | --- | --- | | current | 没有一个 | 线程的当前实例 | | next | 表示返回值的位数的整数值 | 位数指定范围内的整数 | | nextDouble | 双重的双人,双人 | 介于 0.0 及其参数之间的双精度数字两个参数之间的双倍数 | | nextInt | int,int | 参数之间的整数 | | nextLong | 长的长,长 | 0 与其参数之间的长数字它的参数之间有一个很长的数字 | | setSeed | 长的 | 抛出 UnsupportedOperationException |

另见

其使用示例见使用可重复使用的同步屏障移相器配方的*。*