B.2 并发

Java 8中引入了多个与并发相关的更新。首当其冲的当然是并行流,第7章详细讨论过。另外一个就是第16章中介绍的CompletableFuture类。

除此之外,还有一些值得注意的更新。比如,Arrays类现在支持并发操作了。B.3节会讨论这些内容。

本节想要围绕java.util.concurrent.atomic包的更新展开讨论。这个包的主要功能是处理原子变量(atomic variable)。除此之外,我们还会讨论ConcurrentHashMap类的更新,它现在又新增了几个方法。

B.2.1 原子操作

java.util.concurrent.atomic包提供了多个对数字类型进行操作的类,比如AtomicIntegerAtomicLong,它们支持对单一变量的原子操作。这些类在Java 8中新增了更多的方法支持。

  • getAndUpdate——以原子方式用给定的方法更新当前值,并返回变更之前的值。
  • updateAndGet——以原子方式用给定的方法更新当前值,并返回变更之后的值。
  • getAndAccumulate——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之前的值。
  • accumulateAndGet——以原子方式用给定的方法对当前及给定的值进行更新,并返回变更之后的值。

下面的例子向我们展示了如何以原子方式比较一个现存的原子整型值和一个给定的观测值(比如10),并将变量设定为二者中较小的一个。

  1. int min = atomicInteger.accumulateAndGet(10, Integer::min);
AdderAccumulator

多线程的环境中,如果多个线程需要频繁地进行更新操作,且很少有读取的动作(比如,在统计计算的上下文中),Java API文档中推荐大家使用新的类LongAdderLongAccumulatorDoubleAdder以及DoubleAccumulator,尽量避免使用它们对应的原子类型。这些新的类在设计之初就考虑了动态增长的需求,可以有效地减少线程间的竞争。

LongAddrDoubleAdder类都支持加法操作,而LongAccumulatorDoubleAccumulator可以使用给定的方法整合多个值。比如,可以像下面这样使用LongAdder计算多个值的总和。

代码清单 B-1 使用LongAdder计算多个值之和

  1. LongAdder adder = new LongAdder(); ←---- 使用默认构建器,初始的sum值被置为0
  2. adder.add(10); ←---- 在多个不同的线程中进行加法运算
  3. // ...
  4. long sum = adder.sum(); ←---- 到某个时刻得出sum的值

或者,你也可以像下面这样使用LongAccumulator实现同样的功能。

代码清单 B-2 使用LongAccumulator计算多个值之和

  1. LongAccumulator acc = new LongAccumulator(Long::sum, 0);
  2. acc.accumulate(10); ←---- 在几个不同的线程中累计计算值
  3. // ...
  4. long result = acc.get(); ←---- 在某个时刻得出结果

B.2.2 ConcurrentHashMap

ConcurrentHashMap类的引入极大地提升了HashMap现代化的程度,新引入的ConcurrentHashMap对并发的支持非常友好。ConcurrentHashMap允许并发地进行新增和更新操作,因为它仅对内部数据结构的某些部分上锁。因此,和另一种选择,即同步式的Hashtable比较起来,它具有更高的读写性能。

  • 性能

为了改善性能,要对ConcurrentHashMap的内部数据结构进行调整。典型情况下,map的条目会被存储在桶中,依据键生成散列值进行访问。但是,如果大量键返回相同的散列值,由于桶是由List实现的,它的查询复杂度为O(n),这种情况下性能会恶化。在Java 8中,当桶过于臃肿时,它们会被动态地替换为排序树(sorted tree),排序树的查询复杂度为O(\log(n))。注意,这种优化只有当键是可以比较的(比如String或者Number类)时才可能发生。

  • 类流操作

ConcurrentHashMap支持三种新的操作,这些操作和你之前在流中所见的很像。

  • forEach——对每个键值对进行特定的操作。
  • reduce——使用给定的精简函数(reduction function),将所有的键值对整合出一个结果。
  • search——对每一个键值对执行一个函数,直到函数的返回值为一个非空值。
    以上每一种操作都支持四种形式,接受使用键、值、Map.Entry以及键值对的函数。

  • 使用键和值的操作(forEachreducesearch)。

  • 使用键的操作(forEachKeyreduceKeyssearchKeys)。
  • 使用值的操作 (forEachValuereduceValuessearchValues)。
  • 使用Map.Entry对象的操作(forEachEntryreduceEntriessearchEntries)。
    注意,这些操作不会对ConcurrentHashMap的状态上锁。它们只会在运行过程中对元素进行操作。应用到这些操作上的函数不应该对任何的顺序,或者其他对象,抑或在计算过程发生变化的值,有依赖。

除此之外,你需要为这些操作指定一个并发阈值。如果经过预估当前map的大小小于设定的阈值,那么操作会顺序执行。使用值1开启基于通用线程池的最大并行。使用值Long.MAX_VALUE设定程序以单线程执行操作。

下面这个例子中,我们使用reduceValues试图找出map中的最大值:

  1. ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
  2. Optional<Integer> maxValue =
  3. Optional.of(map.reduceValues(1, Integer::max));

注意,对intlongdouble,它们的reduce操作各有不同(比如reduceValuesToIntreduceKeysToLong等)。

  • 计数

ConcurrentHashMap类提供了一个新的方法,名叫mappingCount,它以长整型long返回map中映射的数目。我们应该尽量使用这个新方法,而不是老的size方法,size方法返回的类型为int。这是因为映射的数量可能是int无法表示的。

  • 集合视图

ConcurrentHashMap类还提供了一个名为KeySet的新方法,该方法以Set的形式返回ConcurrentHashMap的一个视图(对map的修改会反映在该Set中,反之亦然)。你也可以使用新的静态方法newKeySet,由ConcurrentHashMap创建一个Set