java核心技术卷I-线程安全(一)

线程安全的集合

如果多线程要并发地修改一个数据结构, 例如散列表, 那么很容易会破坏这个数据结构
(有关散列表的详细信息见第 9 章) 。例如, 一个线程可能要开始向表中插入一个新元素。假定在调整散列表各个桶之间的链接关系的过程中, 被剥夺了控制权。如果另一个线程也开始遍历同一个链表,可能使用无效的链接并造成混乱, 会抛出异常或者陷人死循环。
可以通过提供锁来保护共享数据结构, 但是选择线程安全的实现作为替代可能更容易些。当然,前一节讨论的阻塞队列就是线程安全的集合。

高效的映射、集和队列

java.util.concurrent 包提供了映射、 有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap > ConcurrentSkipListSet 和 ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。与大多数集合不同,size 方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。
有些应用使用庞大的并发散列映射,这些映射太过庞大, 以至于无法用 size 方法得到它的大小, 因为这个方法只能返回 int。对于一个包含超过 20 亿条目的映射该如何处理? JavaSE 8 引入了一个 mappingCount 方法可以把大小作为 long 返回。
集合返回弱一致性( weakly consistent) 的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会拋出 ConcurrentModificationException 异常。与之形成对照的是, 集合如果在迭代器构造之后发生改变,java.util 包中的迭代器将抛出一个 ConcurrentModificationException 异常。
并发的散列映射表, 可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达 16 个写者线程同时执行。可以有更多的写者线程,但是, 如果同一时间多于 16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而, 恐怕没有这种必要。
在 JavaSE 8 中,并发散列映射将桶组织为树, 而不是列表, 键类型实现了 Comparable, 从而可以保证性能为 o(log(n))

映射条目的原子更新

ConcurrentHashMap 原来的版本只有为数不多的方法可以实现原子更新。
假设多个线程会遇到单词,我们想统计它们的频率,下面的代码不是线程安全的

 Long oldValue = map.get(word);
    Long newValue = oldValue == null ? 1: oldValue + 1;
    map.put(word, newValue); // Error-might not replace oldValue

可能会有另一个线程在同时更新同一个计数。
如果多个线程修改一个普通的 HashMap,它们会破坏内部结构 (一个链表数组)。有些链接可能丢失, 或者甚至会构成循环,使得这个数据结构不再可用。对于ConcurrentHashMap 绝对不会发生这种情况。在上面的例子中,get 和 put 代码不会破坏数据结构。不过,由于操作序列不是原子的,所以结果不可预知。
传统的做法是使用 replace 操作, 它会以原子方式用一个新值替换原值,前提是之前没有其他线程把原值替换为其他值。必须一直这么做, 直到 replace 成功:

do
{
	oldValue = map.get(word);
	newValue = oldValue = null ? 1 : oldValue + 1;
} while (!nap.replace(word, oldValue, newValue));

或者, 可以使用一个 ConcurrentHashMap<String,AtomicLong>, 或者在 Java SE 8中,还可以使用 ConcurrentHashMap<String,LongAdder>。更新代码如下:

map.putlfAbsent(word, new LongAdder());
map.get(word).increment();

第一个语句确保有一个 LongAdder 可以完成原子自增。由于 putlfAbsent 返回映射的的
值(可能是原来的值, 或者是新设置的值,) 所以可以组合这两个语句:

map.putlfAbsent(word, new LongAdder()).increraent();

Java SE 8 提供了一些可以更方便地完成原子更新的方法。调用 compute 方法时可以提供一个键和一个计算新值的函数。这个函数接收键和相关联的值(如果没有值,则为 mill), 它会计算新值。例如,可以如下更新一个整数计数器的映射:

map.compute(word , (k, v) -> v = null ? 1: v + 1);

ConcurrentHashMap 中不允许有 null 值。有很多方法都使用 null 值来指示映射中某个给定的键不存在。
另外还有 computelfPresent 和 computelf bsent 方法,它们分别只在已经有原值的情况下计算新值,或者只有没有原值的情况下计算新值。可以如下更新一个 LongAdder 计数器映射:

map.computelfAbsent(word , k -> new LongAdder()).increment() ;

这与之前看到的 putlfAbsent 调用几乎是一样的,不过 LongAdder 构造器只在确实需要一个新的计数器时才会调用。
首次增加一个键时通常需要做些特殊的处理。利用 merge 方法可以非常方便地做到这一点。这个方法有一个参数表示键不存在时使用的初始值。否则, 就会调用你提供的函数来结合原值与初始值。(与 compute 不同,这个函数不处理键)

map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);

或者,更简单地可以写为:

map.merge(word, 1L, Long::sum);

如果传入 compute 或 merge 的函数返回 null, 将从映射中删除现有的条目。

对并发散列映射的批操作

Java SE 8 为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全地执行。批操作会遍历映射,处理遍历过程中找到的元素。无须冻结当前映射的照。除非你恰好知道批操作运行时映射不会被修改, 否则就要把结果看作是映射状态的一个近似。
有 3 种不同的操作:

搜索(search) 为每个键或值提供一个函数,直到函数生成一个非 null 的结果。然后搜 索终止,返回这个函数的结果。
归约(reduce) 组合所有键或值, 这里要使用所提供的一个累加函数。
forEach 为所有键或值提供一个函数

每个操作都有 4 个版本:

operationKeys: 处理键。
operatioriValues: 处理值。
operation: 处理键和值。
operatioriEntries: 处理 Map.Entry 对象。

对于上述各个操作, 需要指定一个参数化阈值。如果映射包含的元素多于这个阈值, 就会并行完成批操作。如果希望批操作在一个线程中运行,可以使用阈值Long.MAX_VALUE。如果希望用尽可能多的线程运行批操作,可以使用阈值 1。

U searchKeys(long threshold, BiFunction<? super K , ? extends U> f)
U searchValues(long threshold, BiFunction<? super V, ? extends U> f)
U search(long threshold, BiFunction<? super K, ? super V,? extends U> f)
U searchEntries(long threshold, BiFunction<Map.Entry<K, V>, ? extends U> f)

例如, 假设我们希望找出第一个出现次数超过 1000 次的单词。需要搜索键和值:

String result = map.search(threshold, (k, v) -> v > 1000 ? k : null);

result 会设置为第一个匹配的单词,如果搜索函数对所有输人都返回 null, 则返回null。
forEach方法有两种形式。第一个只为各个映射条目提供一个消费者函数, 例如:

map.forEach(threshold,(k, v) -> System.out.println(k + " -> " + v));

第二种形式还有一个转换器函数, 这个函数要先提供, 其结果会传递到消费者:

map.forEach(threshold,
(k, v)-> k + " -> " + v
,// Transformer
System.out::println); // Consumer

转换器可以用作为一个过滤器。只要转换器返回 null , 这个值就会被悄无声息地跳过。
reduce 操作用一个累加函数组合其输入。例如,可以如下计算所有值的总和:

Long sum = map.reduceValues(threshold, Long::sum);

与 forEach 类似,也可以提供一个转换器函数。可以如下计算最长的键的长度:

Integer maxlength = map.reduceKeys(threshold,
String::length, // Transformer
Integer::max); // Accumulator

转换器可以作为一个过滤器,通过返回 null 来排除不想要的输入。

Long count = map. reduceValues(threshold,
v -> v > 1000 ? 1L : null ,
Long::sum);

如果映射为空, 或者所有条目都被过滤掉, reduce 操作会返回 null。如果只有一个元素, 则返回其转换结果, 不会应用累加器。
对于 int、 long 和 double 输出还有相应的特殊化操作, 分别有后缀 Tolnt、 ToLong 和ToDouble。需要把输入转换为一个基本类型值,并指定一个默认值和一个累加器函数。映射为空时返回默认值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

局外人一枚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值