如何保证集合是线程安全的?

典型回答

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),可以调用Collections工具类提供的包装方法,来获取一个同步的包装容器,例如Collections.synchronizedMap()。但是它们都是利用非常粗粒度的同步方式,在高并发情况下的性能比较低下。

另外,更加普遍的选择是利用并发包(java.util.concurrent)提供的线程安全容器类:

  • 各种并发容器,比如ConcurrentHashMap、CopyOnWriteArrayList。
  • 各种线程安全队列(Queue/Deque),比如ArrayBlockingQueue、SynchronousQueue。
  • 各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的synchronized方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远远优于早期的简单同步实现。

知识扩展

1、为什么需要ConcurrentHashMap?

首先,Hashtable本身比较低效,因为它的实现基本就是将put、get、size等方法简单粗暴地加上“synchronized”。这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其它线程只能等待,大大降低了并发操作的性能。

上一讲已经提到HashMap不是线程安全的,并发情况或导致类似CPU占用100%等一些问题。

那么能不能利用Collections提供的同步包装器来解决问题呢?以下代码片段摘自Collections:

private static class SynchronizedMap<K,V>
  implements Map<K,V>, Serializable {
  private final Map<K,V> m; // Backing Map
  final Object mutex;  // Object on which to synchronize
  // ...
  public int size() {
    synchronized (mutex) {return m.size();}
  }
}

我们发现同步包装器指示利用输入Map构造了另一个同步版本,所有操作虽然不再声明成为synchronized方法,但是还是利用了“this”作为互斥的mutex,没有真正意义上的改进!

所以,Hashtable或者同步包装版本都只适合在非高度并发的场景下。

2、ConcurrentHashMap分析

再来看看ConcurrentHashMap是如何设计实现的,为什么它能大大提供并发效率。

首先需要强调,ConcurrentHashMap的设计实现其实一直在演化,比如在Java 8中就发生了非常大的变化。

早期的ConcurrentHashMap其实现是基于:

  • 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组。和HashMap类似,哈希相同的条目也是以链表形式存放。
  • HashEntry内部使用volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用sun.misc.Unsafe提供的底层能力,比如volatile access,去直接完成部分操作,以最优化性能。毕竟Unsafe中的很多操作都是JVM intrinsic优化过的。

参考下面这个早期ConcurrentHashMap内部结构示意图,其核心是利用分段设立,在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似Hashtable整体同步的问题,大大提高了性能。

在构造的时候,Segment的数量由所谓的concurrentcyLevel决定,默认是16,也可以在相应构造函数直接指定。注意,Java需要它是2的整数次幂,例如输入15,将会被自动调整到16。

分离锁看似完美,但是在size方法实现时会有副作用。试想,如果不进行同步,简单的计算所有Segment的总值,可能会因为并发put,导致结果不准确;但是直接锁定所有Segment进行计算,就会变得非常昂贵。其实,分离锁也给包括Map初始化等操作带来类似的副作用。

所以,ConcurrentHashMap的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重复次数2),来试图获得可靠值。如果没有监控到发生变化,就直接返回,否则将获取锁进行操作。

3、Java 8对ConcurrentHashMap的改进

  • 总体结构上,它的内部存储变得和上一讲中介绍的HashMap结构非常相似,同样是大的桶数组,然后内部也是一个个链表结果,同步的粒度要更细致一些。
  • 其内部仍然有Segment定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。
  • 因为不再使用Segment,初始化操作大大简化,修改为lazy-load形式。这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。
  • 数据存储利用volatile来保证可见性。
  • 使用CAS(Compare And Swap)等操作,在特定场景进行无锁并发操作。
  • 使用Unsafe、LongAdder之类底层手段,进行极端情况的优化。

【完】

猜你喜欢

转载自blog.csdn.net/qweqwruio/article/details/81349627