Java并发——线程安全的集合

1.线程安全的集合:

一般在并发情况下操作一些集合的时候,例如散列表,可能就会出现线程之间因为互相剥夺资源导致异常或死循环等情况。这个时候可以使用锁机制来保护共享的数据结构,但是如果使用线程安全的实现会更加的方便,之前讨论的阻塞队列就是线程安全的集合。

2.高效的映射、集和队列:

java.util.concurrent包提供了映射、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分使竞争极小化。与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。对于庞大的并发散列映射,就不能输用size得到大小,因为size返回的是int,对于大于int的数就无法处理,Java SE 8引入了一个mappingCount方法可以把大小作为long返回。

集合返回弱一致性的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改。但是,它们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常。与之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出一个ConcurrentModification异常。

并发的散列映射表,可高效低支持大量的读者和一定数量的写者。默认为16个写者,如果多于16个,其他线程将暂时被阻塞,可以指定更大数量的构造器,但是没有这种必要。在Java SE 8中,并发散列映射将同组织为树,而不是列表,键类型实现了Comparable,从而可以保证性能为O(log(n))。

2.1java.util.concurrent.ConcurrentLinkedQueue<E> 5.0:

ConcurrentLinkedQueue<E>()

构造一个可以被多线程安全访问的无边界非阻塞的队列。

2.2java.util.concurrent.ConcurrentLinkedQueue<E> 6.0:

ConcurrentSkipListSet<E>()

ConcurrentSkipListSet<E>(Comparable<? super E> comp)

构造一个可以被线程安全访问的有序集。第一个构造器要求元素实现Comparable接口。

2.3java.util.concurrent.ConcurrentHashMap<K,V> 5.0/java.util.concurrent.ConcurrentSkipListMap<K,V> 6:

ConcurrentHashMap<K,V>()

ConcurrentHashMap<K,V>(int initialCapacity)

ConcurrentHashMap<K,V>(int initialCapacity, float loadFactor, int concurrencyLevel)

构造一个可以被线程安全访问的散列映射表。initialCapacity集合的初始容量,默认为16。loadFactor负载因,默认为0.75子。concurrencyLevel并发写着线程的估计数目。

ConcurrentSkipListMap<K,V>()

ConcurrentSkipListMap<K,V>(Comparable<? super> comp)

构造一个可以被多线程安全访问的有序的映像表。第一个构造器要求键实现Comparable接口。

3.映射条目的原子更新:

ConcurrentHashMap原来的版本只有为数不多的方法可以实现原子操作,这使得编程多少有些麻烦。例如:

HashMap<String, Long> map = new ConcurrentHashMap<String, Long>;
Long oldValue = map.get(word);
Long newValue = oldValue ==null ? 1 : oldValue + 1;
map.put(word, newValue);

可能会有另外一个线程在同时更新同一个计数。

这里可能有个疑问:为什么原本线程安全的数据结构会允许非线程安全的操作。如果这里是一个普通的HashMap,那么很有可能会因为多线程问题破坏它的内部结构从而导致这个数据结构不可用。ConcurrentHashMap则不会有这种情况。但是由于上面的get和put操作不是原子的,所以结果也就无法预知。

传统的做法是使用replace操作,它会以原子的方式用一个新值替换原值,前提是之前没有其他线程把原值替换为其他值。

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

或者使用一个ConcurrentHashMap<String, AtomicLong>,或者在Java SE 8中,还可以使用ConcurrentHashMap<String, LongAdder>。

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

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

Java SE 8中提供了一个方法compute可以提供一个键和一个计算新值的方法。

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

ConcurrentHashMap中不允许有null值。有很多方法都使用null值来只是映射中某个给定的键不存在。

另外还有computeIfPresent和computeIfAbsent,它们分别只在已经有原值的情况下计算新值或只有没有原值的情况下计算新值。

首次增加一个键时可以使用merge方法。这个方法有一个参数表示不存在时使用的初始值。否则,就会调用你提供的函数来结合原值和初始值。

map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);
//或者
map.merge(word, 1L, Long::sum);

如果传入compute和merge的函数返回null,将从映射中删除现有的条目。还有传入的函数不要太复杂,在运行时可能阻塞对映射的其他更新。

猜你喜欢

转载自blog.csdn.net/qq_38386085/article/details/84074324