目录

 

5.1 BlockingQueue

5.2 BlockingDeque

5.3 CopyOnWrite

5.4 ConcurrentLikedQueue/Deque

5.5 ConcurrentHashMap

5.6 ConcurrentSkipListMap/Set


5.1 BlockingQueue

BlockingQueue是一个带有阻塞功能的队列,在Cocurrent包中,BlockingQueue是一个接口

第五章 并发容器_延迟时间

BlockingQueue是具有阻塞功能的队列,如下源码:

public interface BlockingQueue<E> extends Queue<E> {

    boolean add(E e);//不具有阻塞功能

    boolean offer(E e);//不具有阻塞功能

    void put(E e) throws InterruptedException;//具有阻塞功能

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;//具有阻塞功能

    E take() throws InterruptedException;//具有阻塞功能

    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;//具有阻塞功能

    boolean remove(Object o);//不具有阻塞功能
}

不具有阻塞功能的是BlockingQueue的父接口Queue中的方法。

下面介绍BlockingQueue各种实现

 

5.1.1 ArrayBlockingQueue

数组实现的环形队列,核心源码如下:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** The queued items */
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /** Main lock guarding all access */
    final ReentrantLock lock;//一把锁两个条件

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

}

其put/take函数如下所示:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();//当队列满了,阻塞等待唤醒
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

   private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length) putIndex = 0;
        count++;
        notEmpty.signal();//唤醒队列不为空条件
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();//当队列为空则阻塞等待被唤醒
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length) takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();//唤醒不为满的条件
        return x;
    }

}

5.1.2 LinkedBlockingQueue

单向链表的阻塞队列,因为对头和队尾是两个指针分别操作的,所以需要两把锁和两个条件,同时需要一个AtomicInteger的原子变量记录count数。两把锁的目的是为了更好的高并发处理

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    private final AtomicInteger count = new AtomicInteger();

    transient Node<E> head;

    private transient Node<E> last;

    private final ReentrantLock takeLock = new ReentrantLock();

    private final Condition notEmpty = takeLock.newCondition();

    private final ReentrantLock putLock = new ReentrantLock();

    private final Condition notFull = putLock.newCondition();

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                notFull.await();//队列满了等待
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;//唤醒前需要获取takeLock锁
        takeLock.lock();
        try {
            notEmpty.signal();//通知队列不为空唤醒
        } finally {
            takeLock.unlock();
        }
    }

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            while (count.get() == 0) {
                notEmpty.await();//队列为空,等待
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;//唤醒前需要获取putLock锁
        putLock.lock();
        try {
            notFull.signal();//队列不满唤醒
        } finally {
            putLock.unlock();
        }
    }
}

如上,两把锁是为了更好的并发执行,比如:put和put阻塞,take和take阻塞,put和take不阻塞。并且由于是不用锁,所以唤醒前需要获取需要被唤醒条件的锁。对于count变量是put和take一起操作的,所以需要是原子变量。

 

5.1.3 PriorityBlockingQueue

队列通常是先进先出,而PriorityBlockingQueue是按照元素的优先级从小到大出队列的。原理是实现了Comparable接口。

源码如下:

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {

    private transient Object[] queue;//用数组实现的二叉小根堆

    private transient int size;

    private transient Comparator<? super E> comparator;

    private final ReentrantLock lock;//一把锁+一个条件,没有非满条件

    private final Condition notEmpty;

    public void put(E e) {
        offer(e); // never need to block
    }

    public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);//当超过当前队列最大容量,则进行扩容
        try {
            Comparator<? super E> cmp = comparator;
            if (cmp == null)//没有定义比较操作符,使用元素自带的比较功能
                siftUpComparable(n, e, array);//元素入堆
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + 1;
            notEmpty.signal();//不为空唤醒
        } finally {
            lock.unlock();
        }
        return true;
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null)
                notEmpty.await();//为空等待
        } finally {
            lock.unlock();
        }
        return result;
    }

    private E dequeue() {
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            E result = (E) array[0];//因为是最小二叉堆,堆顶即是要出队的元素
            E x = (E) array[n];
            array[n] = null;
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);//调整堆,也就是执行siftDown操作
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }
}

和ArrayBlockingQueue相似,区别是多了一个二叉堆,用于排序。另一个区别是没有notFull条件,当超出长度进行扩容。

 

5.1.4 DelayQueue

延迟队列,也就是按照延迟时间从小到大出队的PriorityQueue

//放入DelayQueue的元素必须实现Delayed 接口
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);//延迟时间
}

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final PriorityQueue<E> q = new PriorityQueue<E>();//优先级队列

    private final transient ReentrantLock lock = new ReentrantLock();//一把锁+一个条件
    private final Condition available = lock.newCondition();

    public void put(E e) {
        offer(e);
    }

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);//元素放入二叉堆
            if (q.peek() == e) {//如果元素刚好在堆顶,则通知等到线程
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();//取出二叉堆的堆顶元素,也就是延迟时间最小的
                if (first == null)
                    available.await();//队列为空,阻塞等待
                else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0L)//延迟时间小于或者等于0,出队列,返回
                        return q.poll();
                    first = null; // don't retain ref while waiting
                    if (leader != null)
                        available.await();//如果有其他线程在等待,则无限期等待
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);//如果没有其他线程在等待,则根据延迟时间有限期等待
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }
}

5.1.5 SynchronousQueue

是一种特殊的BlockingQueue,它本身没有容量。先调用put(),线程阻塞;直到另外一个线程调用了take(),两个线程才同时解锁。

比如调用三次put,三个线程都会阻塞,直到另外的线程调用3次take(),6个线程才能同时解锁。

源码不贴了,比较复杂,并且具有公平模式和非公平模式,有兴趣可以看114页

 

5.2 BlockingDeque

是一个阻塞的双端队列接口,接口源码如下:

//可以在头部或者尾部插入或者删除
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
    void putFirst(E e) throws InterruptedException;
    void putLast(E e) throws InterruptedException;
    E takeFirst() throws InterruptedException;
    E takeLast() throws InterruptedException;
}

如上所示,BlockingDeque就是继承了BlockingQueue并且实现了双端队列。该接口只有一个实现,就是LinkedBlockingDeque。

核心源码如下所示:

public class LinkedBlockingDeque<E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
    static final class Node<E> {
        E item;
        Node<E> prev;//双向链表
        Node<E> next;
        Node(E x) {
            item = x;
        }
    }
    transient Node<E> first;//队列的头和尾
    transient Node<E> last;
    final ReentrantLock lock = new ReentrantLock();//一把锁+两个条件
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
}

对应的实现和LinkedBlockingQueue基本一样,LinkedBlockingQueue是单向链表,LinkedBlockingDeque是双向链表。

源码不贴了,见122页

 

5.3 CopyOnWrite

原理:写数据之前将数据拷贝一份进行修改,再通过悲观锁或者乐观锁的方式写回。

5.3.1 CopyOnWriteArrayList

和ArrayList一样,CopyOnWriteArrayList的核心数据结构也是一个数组。代码如下:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
}

下面是CopyOnWriteArrayList的几个“读”函数:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    final Object[] getArray() {
        return array;
    }

    public boolean isEmpty() {
        return size() == 0;
    }

    private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

    public boolean contains(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length) >= 0;
    }
}

可以看出“读”函数都没有加锁,保证线程安全是在“写”函数中进行的

public boolean add(E e) {
        synchronized (lock) {//加悲观锁
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//先拷贝数组
            newElements[len] = e;//进行add操作
            setArray(newElements);//把新数组赋值给老数组
            return true;
        }
    }

//其他写函数set remove和add类似

5.3.2 CopyOnwriteArraySet

CopyOnwriteArraySet就是Array实现的一个Set,保证所有元素都不重复,其内部封装的是一个CopyOnWriteArrayList。

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private final CopyOnWriteArrayList<E> al;//封装的CopyOnWriteArrayList
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
    public boolean add(E e) {
        return al.addIfAbsent(e);//不重复加进去
    }
}

 

5.4 ConcurrentLikedQueue/Deque

与AQS内部的阻塞队列类似:同样是基于CAS,同样是通过head/tail指针记录队列头部和尾部,但是指针移动方式不同。在AQS阻塞队列中每次入队,tail一定后移一个位置,每次出队,head一定前移一个位置,以保证head指向队列头部,tail指向队列尾部。

但是ConcurrentLinkedQueue中,head/tail的更新可能落后于节点的入队和出队,因为它不是直接对head/tail指针做CAS操作,而是对Node中的item进行操作的。

下面进行详细分析:

见126页

 

5.5 ConcurrentHashMap

        HashMap在jdk1.7的实现方式是“数组+链表”,在jdk1.8中的实现方式是“数组+链表+二叉树”,这种方式被称为“拉链法”。ConcurrentHashMap在这基础上做了优化,在JDK 7 和JDK 8中实现方式有很大差异,下面分开讨论。

5.5.1 JDK 7中的实现方式

        为了提高并发度,在JDK7中将一个HashMap被拆分成多个子的HashMap。每一个HashMap被称为一个Segment,多线程操作多个Segment相互独立。如下图所示:

第五章 并发容器_java_02

        具体来说,每个Segment都继承自ReentrantLock,Segment的数量等于锁的数量,这些锁彼此之间相互独立,即所谓的“分段锁”。代码如下:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    static final class Segment<K,V> extends ReentrantLock implements Serializable {//分段锁
    ...
    }
    /**
     * The segments, each of which is a specialized hash table.
     */
    final Segment<K,V>[] segments;
}

接下来由构造函数的分析入手,剖析ConcurrentHashMap的实现原理。

1.构造函数分析

//第一个参数是ConcurrentHashMap初始大小
    //第二个参数是负载因子
    //第三个参数是并发度,就是Segment数量 

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {    //保证并发度是2的整数次方
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;//对应后面的参数,方便计算hash使用
        this.segmentMask = ssize - 1;//对应后面的参数,方便计算hash使用
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;//每个Segment的初始大小
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)//保证每个Segment的容量,也是2的整数次方
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);//构造第0个Segment
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//Segment的数组大小是ssize,也就是2的整数次方
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

2.put(...)函数分析

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);//把key映射到一个32位整数
        int j = (hash >>> segmentShift) & segmentMask;//再把该整数映射到第j个Segment
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);//对第j个Segment进行初始化
        return s.put(key, hash, value, false);//找到对应的Segment[j],调用其put
    }

可以看出上段put代码没有加锁操作,因为锁是加在s.put内部的。另外多个线程可能同时调用ensureSegment对Segment[j]进行初始化,在这个函数的内部要避免重复初始化,下面详细分析:

private Segment<K,V> ensureSegment(int k) {
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            int cap = proto.table.length;
            float lf = proto.loadFactor;
            int threshold = (int)(cap * lf);
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//CAS操作保证初始化并发正常
                        break;
                }
            }
        }
        return seg;
    }

segments[j]被成功地初始化了,下面进入内部,了解如何把元素放进去。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            //当执行到这个地方时一定拿到锁了
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;//因为tab.length为2的整数次方,这个地方等价于hash对tab.length取模
                HashEntry<K,V> first = entryAt(tab, index);//定位到第index个HashEntry
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;//修改次数增加
                            }
                            break;//key相等或者hash值相等,不会重复插入,直接返回
                        }
                        e = e.next;//遍历链表
                    }
                    else {//已经遍历到链表尾部,没有发现重复元素
                        if (node != null)//在上面的scanAndLockForPut里面,已经建立了节点
                            node.setNext(first);//把node插入链表头部
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);//新建node插入链表头部
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);//超出阈值,扩容
                        else
                            setEntryAt(tab, index, node);//把node赋值给tab[index]
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
    }

如果tryLock()成功则拿到锁进入上面的代码,如果tryLock()不成功,则进入scanAndLockForPut(key,hash,value),如下:

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        if (node == null) // speculatively create node  创建一个新节点
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                else if (++retries > MAX_SCAN_RETRIES) {    //自旋
                    lock();    //阻塞
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

上面的函数做的事是一拿不到锁,先自旋,然后自旋到一定次数仍拿不到锁,则阻塞;二是在自旋过程中遍历链表,若发现没有重复节点,则新建节点,为后面在插入节省时间。

3.扩容

超过一定阈值后Segment内部会进行扩容,如下:

private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;    //在旧容量的基础上扩容二倍
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;//如果一个节点之前在第i个位置,
那么新的hash表中,一定处于i或者i+oldCapacity位置
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;//寻找链表中最后一个hash值不等于lastIdx的元素
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;//关键的一行:把在lastRun之后的链表
元素直接链接到新hash表中的lastIdx位置,在lastRun之前的所有链表元素,需要在新的位置逐个拷贝
                        // Clone remaining nodes
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            int nodeIndex = node.hash & sizeMask; // 把新节点加入新的hash表
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

关于上面扩容函数代码,有几点需要说明:

①.函数的参数,也就是将要加入的最新节点。在扩容完成之后,把该节点加入新的Hash表。

②.整个数组的长度是2的整数次方,每次按二倍扩容,而hash函数就是对数组长度取模,即node.hash & sizeMake。因此,如果元素之前处于第i个位置,当再次hash时,必然处于第i个或者第i+oldCapacity位置。

③.上面的扩容进行了一次优化,并没有对元素依次拷贝,而是先找到lastRun位置。lastRun到链表末尾的所有元素,其hash值没有变化,所以不需要依次重新拷贝,只需要把这部分链表链接到新链表所对应的位置即可,也就是newTable[index]=lastRun。lastRun之前的元素需要依次拷贝。因此即使把for循环去掉,整个程序的逻辑仍然是正确的。

4.get实现分析

//整个过程完全没有锁,通过UNSAFE.getObjectVolatile函数
    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;第一次hash
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);//第二次hash
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

5.5.2 JDK 8中的实现方式

jdk 8有两大变换:①.没有分段锁,②.引入了红黑树,其原理如下图:

第五章 并发容器_java_03

        如果头节点是Node类型,则尾随它的就是链表,如果头节点是TreeNode类型,则它后面是红黑树。链表和红黑树之间可以相互转换,初始时是链表,当链表中的元素超过某个阈值则把链表转换成红黑树,反之,当红黑树中的元素个数小于某个阈值,则再转换成链表。

        JDK 8与JDK 7对比,JDK 7分段锁的三个好处:

①.减少Hash冲突,避免一个槽中太多元素。

②.提交读与写的并发度。段与段之间相互独立。

③.提供扩容的并发度。扩容的时候,不是ConcurrentHashMap一起扩容,而是每个Segment独立扩容。

        针对这三个好处,JDK 8中的处理方式:

①.使用红黑树,当一个槽里面很多元素时,其查询和更新速度会比链表快很多,Hash冲突的问题由此得到更好的解决。

②.加锁的粒度,并非整个ConcurrentHashMap,而是对每个头节点分别加锁,即并发度就是Node数组的长度,初始长度为16,和JDK 7中初始的Segment的个数相同。

③.并发扩容。在JDK 7中一旦Segment个数初始化时候确立,不能在更改,并发度固定。之后只是在每个Segment内部独立扩容,互不影响,不存在并发扩容问题。但是在JDK 8中,当一个线程要扩容Node数组的时候,其他线程还要读写,因此处理过程很复杂。

总结:JDK 8一方面降低了hash冲突,一方面提升了并发度。

1.下面从构造方法一步步分析:

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));//cap是Node数组长度,保持为2的整数次方
        this.sizeCtl = cap;//sizeCtl用于控制初始化或者并发扩容时候的线程数,只不过其初始值设置为cap
    }

2.初始化:解决多线程初始化可能存在重复初始化问题

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // 自旋等待
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//关键:把sizeCtl设为-1
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//初始化
                        table = tab = nt;
                        sc = n - (n >>> 2);//sizeCtl并非表示数组长度,所以初始化成功后,就不再等于数组长度,
而是n-(n>>>2)=n-n/4=0.75n,表示下一次扩容的阈值
                    }
                } finally {
                    sizeCtl = sc;//把sizeCtl设回去
                }
                break;
            }
        }
        return tab;
    }

多个线程竞争通过sizeCtl进行CAS操作实现的。如果某个线程成功把sizeCtl设置为-1,它就拥有初始化权利,进入初始化代码块,等初始化完成,再把sizeCtl设置回去,其他线程则一直执行while循环,自旋等待,直到数组不为null,即初始化结束,退出整个函数。

因为初始化工作量很小,所以此处选择的策略是让其他线程一直等待,而没有帮助其初始化。

3.put(...)实现分析

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<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();//分支1 整个数组初始化
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//分支2 第i个元素初始化
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);//分支3 扩容
            else {//分支4 放入元素
                V oldVal = null;
                synchronized (f) {//关键一句:加锁
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {//链表
                            binCount = 1;
                            for (Node<K,V> 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<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) {//如果是链表,binCount累加
                    if (binCount >= TREEIFY_THRESHOLD)//TREEIFY_THRESHOLD初始是八
                        treeifyBin(tab, i);//超过阈值,转换成红黑树
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//总元素个数累加1
        return null;
    }

上面for循环4个分支:

第一个分支,是整个数组初始化,前面讲过了;

第二个分支,是所在的槽是空的,说明该元素是该槽的第一个元素,直接新建一个头节点,然后返回;

第三个分支,说明该槽正在进行扩容,帮助其扩容;

第四个分支,就是把元素放入槽内。

4.扩容

详细见143页

 

5.6 ConcurrentSkipListMap/Set

ConcurrentSkipListMap/Set是key有序的hashMap,实现了NavigableMap接口,此接口又继承SortedMap接口。

原理:基于SkipList(跳查表)来实现的