ConcurrenthashMap与CopyOnWriteArrayList的原理及源码分析

1,810 阅读5分钟

1. ConcurrentHashMap源码原理分析

2.1 HashMap简介

  • HashMap是一个线程不安全的类,不能在多线程下使用
  • JDK1.7结构:数组+链表(采用拉链法)
  • JDK1.8结构:数组+链表/红黑树(链表长度要大于阈值8)

2.1 JDK1.7的ConcurrentHashMap的实现

  • JDK7中,ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,任然是数组+链表组成的拉链法
  • 每个Segment独立上ReentrantLock锁,每个Segment之间互不影响,提高了并发效率(Segment继承自ReentrantLock)
  • ConcurrentHashMap默认有16个segment,所以最多支持16个线程并发写(操作在不同的segment上时)。默认值在初始化的时候可以指定,但是一旦初始化过后,就不可以扩容。但是每个Segment内部是可以扩容的

2.2 JDK1.8的ConcurrentHashMap的源码分析

  • 根本没有借鉴JDK1.7,而是重写了一遍。。。
  • JDK1.8中的ConcurrentHashMap结构和1.8中的HashMap结构是相似的,也是数组+链表/红黑树(阈值也是8不过还要满足table.length>=MIN_TREEIFY_CAPACITY 这个值是64)

  • put方法

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
      	// key-value的值不能为空
        if (key == null || value == null) throw new NullPointerException();
      	// 计算hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
          	// table如果为空,或者长度为零就执行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
          	// 找出节点需要放置的位置如果为空,然后用CAS来赋值
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
          	// 如果处于MOVED状态 就帮助转换
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
          	// 如果table上要放的位置不为空就执行下列操作
            else {
                V oldVal = null;
              	// 锁住当前table上的位置
                synchronized (f) {
            				
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                              	//key相同就替换
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                              	// 找不到相同的就插入到最尾部
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                      	// 如果数组下方的链式结构是红黑树 就按红黑树处理放置
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
              	// 检查是否满足阈值
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                      	// 满足时就把链表转成红黑树 
                      	// 注意此方法里面还有一个判断tab.length小于64的不转化
                        treeifyBin(tab, i);
                  	// 如果老值不为空就返回
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

  • get方法
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
      	// 计算hash值
        int h = spread(key.hashCode());
      	//	 排除为空的情况,并找到对应位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
          	// 如果相等就直接在table上取值
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
          	// 在红黑树中找值
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
          	// 在链表中找值
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

2.3 对比JDK1.7与1.8

  • 首先是数据结构上:
    • 1.7是segment数组,+Segment(类似HashMap的结构)
    • 1.8是数据+链表/红黑树与HashMap类似
  • 并发上:
    • 1.7是使用ReentrantLock锁住每个Segment
    • 1.8是使用CAS+synchronized
  • 为什么超过8要使用红黑树
    • 首先链表的结构存储要比红黑树存储节省空间
    • 而链表在查询上又没有红黑树块
    • 这个时候就需要一个边界,源码作者做了一个泊松分布运算,在链表达到8时的概率已经非常小了。而链表长度为8时,查找费时也不大。概率只有千万分之几

2.4 线程安全问题

ConcurrenthashMap并发下单独操作的确是安全的,但是组合操作就未必了。所以如果在多线程情况下,有多步操作ConcurrenthashMap的时候需要额外留心

  • 如:如果要修改一个值:可以使用boolean replace(key, oldValue, newValue)来修改,而不是先get然后put, 这个方法类似于CAS的思想
  • 此外还有putIfAbsent(key, value) ,先判断有没有这个值,如果没有就put,有就取出来给你

3. CopyOnWriteArrayList源码原理分析

3.1 使用场景

  • 是用于替代Vector和SynchronizedList的,相较于Vector和SynchronizedList有更好的并发性能
  • Copy-on-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set
  • 主要适用于:对于读操作有快速要求的,即是:读快写慢

3.2 读写规则

  • 我们都知道读写锁的规则是:读写互斥,写写互斥
  • CopyOnWrite则做了一个升级:读取是完全不加锁的,并且写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待。
  • 此外,我们可以在迭代中可以进行删改元素,看一个案例
/**
 * CopyOnWriteArrayList可以在迭代中修改数组内容,而ArrayList不行
 * @author yiren
 */
public class CopyOnWriteArrayListExample01 {
    public static void main(String[] args) {
        // ArrayList<String> list = new ArrayList<>();
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(list);
            String next = iterator.next();
            System.out.println(next);

            if (next.equals("2")) {
                list.remove("3");
            }

            if (next.equals("4")) {
                list.add("3 add");
            }
        }
    }
}
[1, 2, 3, 4, 5]
1
[1, 2, 3, 4, 5]
2
[1, 2, 4, 5]
3
[1, 2, 4, 5]
4
[1, 2, 4, 5, 3 add]
5

Process finished with exit code 0
  • 结果输出和list中的元素不对应。CopyOnWriteArrayList是这个思想,迭代你可以改,但是你改你的,我迭代我的,它内部是副本机制,这个和ArrayList的迭代器不一样,ArrList里面有一个modCount值来判断你迭代过程中是否修改的

    final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    
    • 这个expectedModCount是在迭代器创建前,从ArrayList对象中获取的,原有ArrayList对象左右删改,那么modCount就会和expectedModCount不一值,此时就会快速失败了。

3.3 实现原理

  • CopyOnWrite:在写入操作的时候,它会先copy一份到新内存上,然后再修改,修改完成,再把原来的指针指过去,就OK。

  • 这个过程就导致了,你在迭代的时候,迭代的内存还是老内存上的值,而不是修改过后的值

  • 所以注意:每次修改或添加都会创建新副本,使之读写分离,而旧的内存数据是不会变的。

  • 我们再看一个案例

/**
 * @author yiren
 */
public class CopyOnWriteArrayListExample02 {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");
        Iterator<String> itr1 = list.iterator();
        list.add("4");
        Iterator<String> itr2 = list.iterator();

        itr1.forEachRemaining(System.out::print);
        System.out.println();
        itr2.forEachRemaining(System.out::print);
    }
}
123
1234
Process finished with exit code 0
  • 在CopyOnWrite的迭代器使用上,即使你修改了,它的迭代内容也只取决于他创建时候的集合的数据内容。而不取决于实际list是否修改。

  • 所以迭代过程可能会出现数据过期问题

3.4 存在的缺点

  • 数据一致性问题:也就是上面所提到的,它只能保证最终数据一致性,而不保证数据实时一致性。如果对写入实时响应的需求,不推荐使用。
  • 内存浪费:CopyOnWrite的写是复制的机制,写操作的时候就一定会复制一份。这会很浪费内存

3.5 源码分析

  • 首先CopyOnWriteArrayList是一个数组的列表集合,它的根本存储就是数组
    private transient volatile Object[] array;
  • 多线程同时写入的时候是ReentrantLock。
    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();
  • 它的创建,构造函数可想而知,也就是给一个空数组。
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
  • 但是它提供了一个可以直接放集合的构造函数,把数据先放入数组,然后直接指过去

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

  • add方法
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //添加的时候先上锁
        lock.lock();
        try {
            // copy一份到新数组,数组长度+1
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 新值放到末尾,把指针指过去
            newElements[len] = e;
            // 最后返回true 并释放锁
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
  • get方法
    • 没有任何加锁,直接返回对应的值
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }