第五章 并发增强

一、原子值
1.从java5开始,java.util.concurrent.atomic包提供了用于支持无锁可变变量的类,可以安全的生成一组数字
--------------------------------------------------------------------------------------------------------------------
public static AtomicLong nextNumber = new AtomicLong ();
long id = nextNumber . incrementAndGet() ;
--------------------------------------------------------------------------------------------------------------------
incrementAndGet() 方法会自动将 AtomicLong 的值加1并返回增加后的值,并且产生新值的过程不能被打断,它可以保证即便有多个线程同时并发访问同一个实例,也能够计算并返回正确的值
2.Java5中提供了很多设置、增加、减少值的原子操作,但是如果要进行更复杂的更新操作,就必须使用 compareAndSet 方法。因为当在多线程中设置新值时,有可能被另一个线程捷足先登更新成功,此时 compareAndSet 方法会返回一个false并且不会设置新值, compareAndSet 方法会映射为一个底层的处理器方法,远比使用一个锁要快得多
--------------------------------------------------------------------------------------------------------------------
do{
oldValue = largest . get() ; //largest是原子值对象
newValue = Math.max(oldValue,observed);
}while (! largest . compareAndSet (oldValue,newValue));
--------------------------------------------------------------------------------------------------------------------
在Java8中只需要提供一个用来更新值的lambda表达式
--------------------------------------------------------------------------------------------------------------------
largest. updateAndGet (x->Math.max(x,observed));
largest. accumulateAndGet (observed,Math::max);
--------------------------------------------------------------------------------------------------------------------
除此之外Java8还提供了返回原始值的 getAndUpdate getAndAccumulate 方法
3.当有大量线程访问统一个原子值时,由于乐观锁更新需要太多次重试,因此会导致性能严重下降,Java8提供了 LongAdder LongAccumulator 来解决该问题。 LongAdder 由多个变量组成,这些变量累加得值即为当前值。由于通常情况下都是直到所有工作完成后才需要总和值,所以这种方法效率很高。
(1).如果环境中存在高度竞争时,应当使用 LongAdder 来代替 AtomicLong ,两者方法命名有些不同, increment 用来将计数器自增1, add 方法用来加上某个数值, sum 方法用来获取总和值
需要注意的是, increment 方法不会返回原始值,只会抹杀掉将总和值拆分为多个被加数所带来的性能提升
--------------------------------------------------------------------------------------------------------------------
final LongAdder adder = new LongAdder() ;
for(……)
pool.submit(()->{
while(……){
……
if (……) adder. increment() ;
}
});
long total = adder. sum() ;
--------------------------------------------------------------------------------------------------------------------
(2).在 LongAccumulator 的内部,包含a1,a2,……an等多个变量,每个变量都被初始化为中立元素。调用 accumulate 会对某个变量i计算ai = ai + v, get 方法会计算所有变量的总和。在 LongAccumulator 的构造函数中,需要提供操作类型及中立元素
--------------------------------------------------------------------------------------------------------------------
LongAccumulator adder = new LongAccumulator (Long :: sum, 0);
adder. accumulate (value);
--------------------------------------------------------------------------------------------------------------------
4.除了 LongAdder LongAccumulator 类外,Java8还提供了 DoubleAdder 类和 DoubleAccumulator 类用于double类型的值

二、 ConcurrentHashMap 改进
从Java5开始, ConcurrentHashMap 已经变成了并发编程的主力军。
ConcurrentHashMap 是线程安全的,多个线程不需要对内部结构造成破坏就可以添加或删除元素,而且性能也很不错,允许多个线程并发更新哈希表的不同部分,而不会互相阻塞
CocurrentHashMap 不允许有null值, ConcurrentHashMap 中的许多方法用null值来表示映射中不含有指定的键
需要注意的是,因为 size 方法返回的是 int 值,当需要使用数量巨大的并发哈希映射时, size 方法将会不够用。Java8引入了一个 mappingCount 方法用来返回一个反应大小的long型值
1.更新值
原来的 ConcurrentHashMap 只有一些用于原子更新的方法,这会带来一些安全问题。假设多个线程都会访问一些单词,我们希望统计他们的频率
--------------------------------------------------------------------------------------------------------------------
Long oldValue = map.get(word); //map为ConcurrentHashMap<String, Long>
Long newValue = oldValue == null ? 1 : oldValue + 1;
map.put(word, newValue); //另一个线程可能同时在更新相同的计数所以无法替换
--------------------------------------------------------------------------------------------------------------------
这代码显然是线程不安全的,上文中说过, ConcurrentHashMap 是一个线程安全的数据结构,现在竟然允许线程不安全的操作?这是因为出于两种不同的考虑,如果多个线程修改一个普通的 HashMap ,它们可能会破坏内部的结构,导致一些链接可能会丢失或者形成回路,从而导致数据结构不可用,在 ConcurrentHashMap 中永远不可能发生。但由于操作的顺序不是原子的,因此结果也无法预测
此时可以使用以下方法避免线程不安全问题
(1). replace :将一个已知的旧值替换成一个新值
--------------------------------------------------------------------------------------------------------------------
do{
oldValue = map.get(word);
newValue = oldValue ==null ? 1 : oldValue + 1;
}while(!map. replace (word, oldValue, newValue));
--------------------------------------------------------------------------------------------------------------------
(2). ConcurrentHashMap <String, AtomicLong >
ConcurrentHashMap <String, LongAdder >
--------------------------------------------------------------------------------------------------------------------
map. putIfAbsent (word, new LongAdder());
map.get(word). increment() ;
--------------------------------------------------------------------------------------------------------------------
putIfAbsent 方法会在键值不存在时赋值,返回已有的值或者新更新的值,因此可以简写为
--------------------------------------------------------------------------------------------------------------------
map. putIfAbsent (word, new LongAdder()). increment() ;
--------------------------------------------------------------------------------------------------------------------
(3). compute :通过一个键和一个函数来计算出新的值,该函数会获取键和所关联的值(如果没有值则为null),然后计算出新的值
--------------------------------------------------------------------------------------------------------------------
map. compute (word, (k, v) -> v == null ? 1 : v+1);
--------------------------------------------------------------------------------------------------------------------
(4). computeIfPresent computeIfAbsent :在已经存在值和尚未存在值的情况下,才计算新值
--------------------------------------------------------------------------------------------------------------------
map. computeIfAbsent (word,k -> new LongAdder()). increment() ;
--------------------------------------------------------------------------------------------------------------------
(5). merge :第二个参数来表示键尚未存在时的初始值。当已存在时,则调用传入的函数,与 compute 不同,该函数不会处理键
--------------------------------------------------------------------------------------------------------------------
map. merge (word, 1L, (existingValue, newValue) -> existingValue + newValue);
map. merge (word, 1L, Long::sum);
--------------------------------------------------------------------------------------------------------------------
需要注意的是,如果传递给 compute 或者 merge 方法的函数返回null,那么已有的数据项会从映射中删除掉
当使用 compute 或者 merge 方法时,所提供的函数不应该进行大量的工作以及更新映射的其他部分,否则可能会导致其他一些更新映射操作被堵塞
2.批量数据操作
Java8中为并发哈希映射提供了批量操作数据,即使在其他线程同时操作映射时也可以安全的执行。批量数据操作会遍历映射并对匹配的元素进行操作,批量操作有三类:
  • search:对每个键(值)应用一个函数,直到函数返回一个非null的结果
  • reduce:通过提供的累积函数,将所有键(值)组合起来
  • forEach:对所有键(值)应用一个函数
每个操作有四个版本:
  • searchKeys/reduceKeys/forEachKey:对键操作
  • searchValues/reduceValues/forEachValue:对值操作
  • search/reduce/forEach:对键和值操作
  • searchEntries/reduceEntries/forEachEntry:对Map.Entry对象操作
在使用这些操作时,需要指定一个并行阀值,当映射包含的元素数量超过这个阀值,批量操作就以并行方式执行。如果希望批量数据操作在一个线程中运行,请使用 Long.MAX_VALUE 作为阀值,如果希望数据操作尽可能使用更多的线程,则应该使用1作为阀值
(1). search
U search (long threshold, BiFunction <? super k, ? extends U> f)
--------------------------------------------------------------------------------------------------------------------
String result = map. search (threshold, (k, v) -> v > 1000 ? k : null);
--------------------------------------------------------------------------------------------------------------------
(2). forEach :两种形式,第一种只是对每个映射数据项简单的应用一个消费者函数,
第二种形式会额外接收一个转换器函数,会先调用转换器函数,再将结果传递给消费者函数
--------------------------------------------------------------------------------------------------------------------
map. forEach (threshold, (k, v) -> System.out.println(k + " -> " + v));
map. forEach (threshold, (k, v) -> k + " -> " + v, System.out::println);
--------------------------------------------------------------------------------------------------------------------
注意,其转换器函数可以作为一个过滤器,当转换器函数返回null时,值会被直接跳过
(3). reduce :将其如数与一个累加函数结合起来,也有两种形式可提供一个转换器函
数,如果只有一个元素,会返回它转换后的值,并且不会应用累加器函数
--------------------------------------------------------------------------------------------------------------------
Long sum = map. reduceValues (threshold, Long::sum);
Integer maxlength = map. reduceKeys (threshold, String::length, Integer::max);
Long count = map. reduceValues (threshold, v -> v > 1000 ? 1L : null, Long::sum);
--------------------------------------------------------------------------------------------------------------------
(4) reduce 为int、long、double类型的输出提供了专门的方法,分别以ToInt、ToLong
和ToDouble后缀结尾。需要将输入转换为原始类型,并指定一个默认值和累加器函数,会对默认值进行累加,并且当映射为空时返回默认值
--------------------------------------------------------------------------------------------------------------------
long sum = map. reduceValuesToLong (threshold, Long::longValue, 0, Long::sum);
--------------------------------------------------------------------------------------------------------------------
3.Set视图
ConcurrentHashMap 中的 newKeySet 方法会放回一个Set<K>对象,该Set对象是线程
安全的,实际上是对 ConcurrentHashMap <K, Boolean>对象的封装,其所有的值都是Boolean.TRUE,实际上值是什么无所谓,我们都只将其作为Set使用
--------------------------------------------------------------------------------------------------------------------
Set<String> words = ConcurrentHashMap .<String> newKeySet() ;
--------------------------------------------------------------------------------------------------------------------
当然。如果我们拥有一个映射,通过keySet获取Set对象,该Set对象中的元素可以被删除,并且相应的键也会从映射中删除,但是无法向这个Set中添加元素,因为无法添加相应的值。Java8给 ConcurrentHashMap 添加了另一个keySet方法,可以接受一个默认值,以便于向Set中添加元素
--------------------------------------------------------------------------------------------------------------------
Set<String> words = map.keySet(1L);
words.add("Java"); //如果words中不存在"Java",那么它当前有一个值,为1
--------------------------------------------------------------------------------------------------------------------

三、并行数组操作
Arrays类现在对了许多并行化的操作
1.静态方法Arrays. parallelSort 可以对原始类型数组或者对象数组进行排序
--------------------------------------------------------------------------------------------------------------------
String contents = new String(Files.readAllBytes(Paths.get("alice.txt")),
StantdardCharsetsUFT_8);
String[] words = contents.split("[\\P{L}]+");
Arrays. parallelSort (words);
--------------------------------------------------------------------------------------------------------------------
对于这些方法,可以提供一个范围值
--------------------------------------------------------------------------------------------------------------------
Arrays. parallelSort (values, values.length/2, values.length); //对上半部分进行排序
--------------------------------------------------------------------------------------------------------------------
2.方法 parallelSetAll 会将数组中的值按照一个函数的计算结果过滤出来
--------------------------------------------------------------------------------------------------------------------
Arrays. parallelSetAll (values, i->i%10);
--------------------------------------------------------------------------------------------------------------------
3.方法 parallelPrefix 方法将数组中的每个元素替换为指定关联操作的前缀的累累积
假设有数组[1, 2, 3, 4],通过下列计算后得到[1, 1x2, 1x2x3, 1x2x3x4]
--------------------------------------------------------------------------------------------------------------------
Arrays. parallePrefix (values, (x, y) -> x* y);
--------------------------------------------------------------------------------------------------------------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值