一、原子值
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);
--------------------------------------------------------------------------------------------------------------------