Java并发编程 —— 同步容器与并发性容器:关于ConcurrentHashMap

以下是自己在研究《Java 并发编程实战》这本书时做的一些笔记,增加了别人的一些见解和自己的理解,将其整理如下:

1. 同步容器

  • 在Java中,同步容器主要有两类:

一是Vector、HashTable、Stack,他们都是线程安全的

Vector实现了List接口,Vector实际上就是一个数组,和ArrayList类似,但是Vector中的方法都是synchronized方法,即进行了同步措施。

Stack也是一个同步容器,它的方法也用synchronized进行了同步,它实际上是 继承于Vector类

HashTable实现了Map接口,它和HashMap很相似,但是HashTable进行了同步处理,而HashMap没有。

二是Collections中提供的静态方法类

2. 并发容器

Java5.0提供了多种并发容器来改进同步容器的性能,同步容器将所有对容器的访问进行串行化,以实现他们的线程安全性。这种方法的弊端是严重降低并发性,当多个线程竞争容器的锁时,容器的吞吐量会严重降低。

另一方面,并发容器是针对多个线程并发访问设计的,在Java5.0中新增了ConcurrentHashMap,用来代替同步且基于散列的HashTable,以及CopyOneWriteArrayList,用于在遍历操作作为主要操作的情况下代替同步的Vector,在新的ConcurrenMap接口中添加了一些常见的符合操作的支持,例如“如没有则添加”,替换以及有条件删除等。

通过并发容器来代替同步容器,可以极大的提高伸缩性和降低风险。

关于并发性设计中的队列请查看:

3. ConcurrentHashMap

  • ConcurrentHashMap与HashTable的区别:

图片来自网络:
Java并发编程 —— 同步容器与并发性容器:关于ConcurrentHashMap

  1. 在HashTable实现同步是利用Synchronized进行实现的,其实是 针对整张表进行锁定的。即每次锁住整张表让线程独占。

  2. 而ConcurrentHashMap是单独锁住每一个桶(segment),ConcurrentHashMap将哈希表分为16个桶(默认值)诸如get()、put()等方法只是锁住当前需要的桶,而size()方法才会锁住整张表,原来只能一个线程进入,而现在却能接受16个线程的同时并发进入,并发性的提升是显而易见的。

  3. ConcurrentHashMap使用了不同于传统集合的快速失败迭代器的另一种迭代方式。称为弱一致迭代方式在这种迭代方式中,当iterator被创建后集合在发生改变不再是抛出ConcurrentModificationException,取而代之的是在改变时实例化出新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator可以使用原来的老数据,而写线程也可以并发的完成改变,更重要的是保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

  • ConcurrentHashMap的组成结构

图片来自网络:
Java并发编程 —— 同步容器与并发性容器:关于ConcurrentHashMap

  1. ConcurrentHashMap中的主要实体类是三个:ConcurrentHashMap,Segment(桶),HashEntry(节点)

Segment是一个可重入锁ReentrantLock,在CocurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据

一个ConcurrentHashMap里包含一个Segment数组,Segment的结构与HashMap的结构类似,是一种数组和链表结构,一个Segment中包含一个HashEntry数组,每一个HashEntry数组是一个链表结构的元素,每个Segment守护着HashEntry中的元素,当对HashEntry中的元素进行修改时,必须获取他对应的Segment锁。

ConcurrentHashMap与其他并发性容器一起增强了同步容器类:他们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器进行加锁。ConurrentHashMap返回的迭代器具有弱一致性,而并得及时失败。

弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历以有的元素,并可以在迭代器被构造后将修改操作反映给容器。

尽管有这些改进,但任然有一些需要权衡的因素:对于在整个Map上进行计算的方法,例如size和isEmpty,这些方法的语义上被略微减弱以反映容器的并发特性,由于size返回的计算结果可能在计算时已经是过期了,他实际只是一个估计值,因此允许size返回一个近似值而不是一个精确值。

在ConcurrentHashMap中没有实现对Map加锁以独占线程访问,在HashMap和SynchronizedMap中,获得Map的锁能防止其他线程访问这个Map,在一些不常见的情况中需要实现这些功能。

与HashTable和SynchronizedMap相比,ConcurrentHahsMap有着更多的优势以及更少的劣势,因此在大多数的情况下,用ConcurrentHashMap来代替同步Map可以提高代码的伸缩性,只用应用程序需要加锁Map以进行独占访问时,才可以考虑放弃使用ConcurrentHashMap

4. CopyOnWriteArrayList

  1. 先来介绍一下什么是Copy-On-Write(写入时复制)

Copy-On-Write简称COW,是一种用于程序设计中的优化策略,其基本思路是:从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正的把内容copy出去形成一个新的内容再修改,这是一种延时懒惰策略。

  1. 什么是CopyOnWrite容器

CopyOnWrite容器即是写时复制容器,通俗的理解就是 当我们向一个容器中添加元素的时候,首先先将当前容器进行copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。 这样做的好处是我们可以对CopyOnWrite进行并发的读,则不需要进行加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。

“写入时复制容器”返回的迭代器不会抛出ConcurrentModificationException,并且返回的元素迭代器创建时的元素完全一致,而不必考虑之后修改操作所带来的影响。

4.1. 关于CopyOnWriteArrayList

CopyOnWriteArrayList用于代替同步的List(具体指的是Vector),在某些情况下他提供了更好的并发性能,在迭代期间不需要对容器进行加锁或者复制。类似的CopyOnWriteArraySet作用是来代替Set

“写入时复制”容器的线程安全性在于,只要正确发布一个事实不可变的对象,那么在访问该对象时就不需要再进一步的同步。在每次修改时都会创建并重新发布一个新的容器副本,从而实现可变性。

引用场景是读多写少的情况下,比如搜索网站,但是CopyOnWriteArrayList只能保证数据的最终一致性,但是不能保证数据的实时一致性。其次是占用内存,写时复制机制会在内存中同时驻扎两个对象的内存

源码解析:
基于JDK1.8

//add()方法
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();//获取锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);//复制数组
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();//释放锁
    }
}

//get()方法
public E get(int index) {
    return get(getArray(), index);
}

在进行读的时候不需要进行加锁,如果此时有多个线程正在向CopyOnWriteArrayList添加元素,读操作仍然会读到旧数组中的元素。因为写的时候不会锁住当前的CopyOnWriteArrayList

未完。。。