线程并发--CocurrentHashMap和CopyOnWriteArrayList详解

在多线程开发中,我们经常要考虑线程并发的问题,那么如何来避免线程并发代码的数据读写问题呢?

我们常见的HashMap、TreeMap、LinkedList、ArrayList都是线程不安全的,而Java也提供了一些线程安全的容器类:

如:

各种并发容器:CocurrentHashMap、CopyOnWriteArrayLis等;

各种线程安全队列(Queue/Deque):ArrayBlockingQueque、SynchronousQueue等;

各种有序容器的线程安全版本等。

下面我们就来说一说CocurrentHashMap和CopyOnWriteArrayList是如何实现高效线程安全的?

记得当初在刚学习java时,遇到可能存在并发情况时,如数据库的读写时,只要简单的加个synchronized关键字即可,那么并发的问题就解决了。但是这是最低效的并发方式的处理,也就是不管三七二十一,set和get方法都给加个synchronized就完事了。那么怎样才能实现高效并发呢?下面来看下CocurrentHashMap源码关于高效并发问题的解决方案,不讨论所有源码,仅涉及线程安全的相关源码。

1.CocurrentHashMap高效并发源码分析

1.1 volatile关键字

不了解volatile关键字的可以看我收藏的这篇:https://blog.csdn.net/fwt336/article/details/80986409,对volatile 说的很详细。

而在线程并发中,我们就需要用到volatile 的可见特性,来保证并发操作变量的可见性,而对于volatile 的非原子操作,我们可以看CocurrentHashMap是怎么做的。

1.2 volatile的使用

下面来看看CocurrentHashMap源码中关于volatile的应用:

我们都知道,在源码中是通过这个table数组来保存我们存入Map中的key和value值的:

transient volatile Node[] table;

而Node是才是真正对我们的key和value值的封装:

static class Node implements Map.Entry {
    final int hash;
    final K key;
    volatile V val;
    volatile Node next;

    Node(int hash, K key, V val, Node next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    public final K getKey()     { return key; }
    public final V getValue()   { return val; }
    public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
    public final String toString() {
        return Helpers.mapEntryToString(key, val);
    }
    public final V setValue(V value) {
        throw new UnsupportedOperationException();
    }
    ...
}

而在Node中有一个next,如果你想问为啥会有一个next节点在这里呢?那就说明你对Map的存储细节不够熟悉,这里与HashMap是类似的。简单总结下就是,Map虽然是通过数组table来存储数据,但是在table数组中的Node节点,确是通过链表来实现的,因为在存储的时候会发生hash碰撞,但是不同key可能通过hash换算后所对应table数组的index是一样的,所以在数组中同一个index的值会有多个key和value存在,那么我们通过链表就可以接近这个问题,这也就是为什么HashMap不是有序存储的了。而由于数据量太大时,链表的查找性能问题就会很明显了,这时候会对链表进行树化,来优化性能,而树化用的是红黑树。

扯远了,回来=======================================================================

我们看到源码中的val和next都用volatile修饰了,而且在Node的源码中我们也没有看到synchronized这个同步关键字。我们看到get方法也是不需要synchronized关键字的,因为有了volatile的可见性来保证数据的可见性操作。那么它的原子操作又是在哪里实现的呢?

1.3 原子操作的保证synchronized

现在我们知道通过使用volatile修饰val和next之后,get方法是不需要synchronized来修饰的,这样性能就得到了一定的提升。

那么涉及到修改,肯定是在set方法了:

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node[] tab = table;;) {
        Node f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node p;
                        binCount = 2;
                        if ((p = ((TreeBin)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            ...
        }
    }
    addCount(1L, binCount);
    return null;
}
我们看到synchronize同步了变量f这个节点,也就是我们需要操作的value。比起我们直接同步一个方法,性能也会大大提高。也就是我们在设置节点,替换节点,清除节点等对value操作时,只需要同步我们操作的这个节点即可。

2.CopyOnWriteArrayList高效并发源码分析

同样的,CopyOnWriteArrayList也使用了volatile来保证可见性,synchronized进行同步,看数组定义:

private transient volatile Object[] elements;

不同的是:

final transient Object lock = new Object();

还有一个这玩意,lock,没错:

public E set(int index, E element) {
    synchronized (lock) {
        Object[] elements = getArray();
        ...
        return oldValue;
    }
}
public boolean add(E e) {
    synchronized (lock) {
        ...
        return true;
    }
}
private boolean remove(Object o, Object[] snapshot, int index) {
    synchronized (lock) {
        ...
        return true;
    }
}

...

在CocurrentHashMap中,synchronized同步的是当前需要操作的Node节点,而这里使用的是一个Object类实例来作为锁的对象,所有涉及到对elements数组的操作都需要先获取这把锁。这样也就达到了线程同步的作用。

3.总结

所以,从Java提供的线程安全类的源码来看,实现高效并发的方式有:

1.对可变字段使用volatile修饰

2.getXX方法不需要加synchronized关键字

3.涉及到变量的所有修改操作,对需要操作的变量使用synchronized关键字进行同步,或定义一个Object实例充当锁,进行同步

你可能感兴趣的:(java并发)