常用并发容器

106 阅读5分钟

ArrayBlockingQueue

BlockingQueue的有界队列实现,主要字段如下:

/** 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;

/*
 * Concurrency control uses the classic two-condition algorithm
 * found in any textbook.
 */

/** Main lock guarding all access */
final ReentrantLock lock;

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

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

可以看到内部持有一个数组,有一个可重入锁和两个condition,通过这两个codition可以实现生产者消费者模式,重点看一下put和take方法;

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

可以看到当队列满时,会一直阻塞等待notFull条件满足,然后进行入队操作

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}

入队后会调用notEmpty.singal()唤醒take()的线程,从而实现生产者消费者模式。

LinkedBlockingQueue

与ArrayBolckingQueue不同的是,LinkedBlockingQueue持有两把锁,入队和出队的锁是分开的,这样的好处是,入队和出队操作可以并发的执行,提升并发效率。

private final int capacity;

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

可以注意到的是,LinkedBlockingQueue的锁都是非公平锁,而ArrayBlockingQueue可以指定为公平锁。

ConcurrentLinkedQueue

与LinkedBlockingQueue区别的是ConcurrentLinkedQueue属于并发容器,而不是阻塞容器,所以里面并没有lock的身影,那么ConcurrentLinkedQueue是通过什么保证线程安全的呢?答案依然是CAS+volital

volatile E item;
volatile Node<E> next;

/**
 * Constructs a new node.  Uses relaxed write because item can
 * only be seen after publication via casNext.
 */
Node(E item) {
    UNSAFE.putObject(this, itemOffset, item);
}

boolean casItem(E cmp, E val) {
    return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}

void lazySetNext(Node<E> val) {
    UNSAFE.putOrderedObject(this, nextOffset, val);
}

boolean casNext(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

重点看一下offer方法,与LinkedBlockingQueue的对比

        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (p.casNext(null, newNode)) {
                    // Successful CAS is the linearization point
                    // for e to become an element of this queue,
                    // and for newNode to become "live".
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)
                // We have fallen off list.  If tail is unchanged, it
                // will also be off-list, in which case we need to
                // jump to head, from which all live nodes are always
                // reachable.  Else the new tail is a better bet.
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

比LinkedBlockingQueue要复杂的多,重点在于所有的写操作都是用CAS+自旋完成,所以才能保证线程安全,过程主要分为两步:
1.定位tail节点(在ConcurrentLinkedQueue里tail节点不一定是尾结点【延迟更新,主要是为了通过增加read次数减少writeTail的此时来提高并发效率】);
2.将新增节点设置为tail节点的next节点

CoucurrentHashMap

java7中,由分段锁实现并发控制,put时步骤:
1.由key定位到具体的segment(继承于ReentrantLock),
2.每个segment中持有HashEntry<K,V>[],然后通过对key做hash后定位到具体的bucket,也就是确定在数组的哪个位置
3.如果此时发生hash冲突,则比较key值,key不一样则形成链表,加在tail.next java8中,put流程: 1.首先对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在 table中的位置;
2.如果当前table数组还未初始化,先将table数组进行初始化操作;
3.如果这个位置是null的,那么使用CAS操作直接放入;
4.如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果该节点fh==MOVED(代表forwardingNode,数组正在进行扩容)的话,说明正在进行扩容;
5.如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到key相同的节点,则只需要覆盖该结点的value值即可。否则依次向后遍历,直到链表尾插入这个结点;
6.如果这个节点的类型是TreeBin的话,直接调用红黑树的插入方法进行插入新的节点;
7.插入完节点之后再次检查链表长度,如果长度大于8,就把这个链表转换成红黑树;
8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容

CopyOnWriteArrayList

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();
        }
    }

1、采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据;
2、数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。
3、由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。