Java 并发容器

文章目录

  • 1.并发集合简介
  • 2.ConcurrentHashMap
  • 3.CopyOnWriteArrayList
  • 4.ConcurrentLinkedQueue
  • 5.BlockingQueue
  • 6.ConcurrentSkipListMap

1.并发集合简介

  • ConcurrentHashMap:线程安全的HashMap,加锁力度更小
  • CopyOnWriteArrayList:适用于在读多写少的场景下,性能优于ArrayList
  • ConcurrentLinkedQueue:线程安全的LinkedList,是用链表实现的高效并发队列
  • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合作为数据共享的通道
  • ConcurrentSkipListMap:跳表。在Redis中被用过。

2.ConcurrentHashMap

ConcurrentHashMap详解

3.CopyOnWriteArrayList

在一些应用场景中,读操作会远远大于写操作。比如一些系统级别的信息,往往只需要加载或修改很少次数,但会被频繁访问。

CopyOnWriteArrayList将读取的性能发挥到极致,对它来说读取是完全不用加锁的,而且写入也不会阻塞读取操作,只有写入和写入之间需要同步等待。

如何做到的?

从这个类的名字可以看出,CopyOnWrite就是在写入操作的时候,进行一次自我复制。换句话说,我不对这个List直接修改,而是复制一份去修改它的副本,再把它的副本替换原来的数据,所以写操作不会影响读操作。

有关读操作的代码:

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

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

private E get(Object[] a, int index) {
	return (E) a[index];
}

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

可以看出读操作普普通通,没有任何锁操作和同步控制,原因就是内部数组array不会发生修改,只会被替换,因此可以保证安全。

有关写操作的代码:

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

第7行代码对内部元素进行了复制生成了一个新数组newElements,在第9行使用新数组替代老数组,修改就完成了整个过程不影响读取,修改完后读取线程就会立刻察觉(因为array用volatile修饰

关于volatile可以查看我这篇文章:volatile关键字详解

4.ConcurrentLinkedQueue

ConcurrentLinkedQueue是在高并发环境下性能最好的队列,它的设计极为复杂。

内部链表的节点:

private static class Node<E> {
	volatile E item;
	volatile Node<E> next;

item是用来表示目标元素的,是个泛型。next表示下一元素。

对Node进行操作时使用了CAS操作。

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

casItem()表示设置当前Node的item值。它需要两个参数,第一个参数cmp为期望值,第二个参数val为设置目标值。当当前值等于cmp时,就会将目标设置为val。casNext()也是类似的不过它用来设置next。

ConcurrentLinkedQueue有两个重要字段:head、tail,表示链表的头部和尾部。但是ConcurrentLinkedQueue内部比较复杂,tail实际上的更新并不是及时的,可能会产生拖延现象,也就是它不是实时指向链表末尾。每插入两次更新一下tail

ConcurrentLinkedQueue添加元素的offer()方法真是鬼才设计,它可以告诉你ConcurrentLinkedQueue的高性能是怎么来的。

public boolean offer(E e) {
    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)) {
                // 每两次更新一下tail
                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)
            // 遇到哨兵节点,返回head,从头开始遍历
            // 但如果tail被修改,返回tail(可能修改对了)
            p = (t != (t = tail)) ? t : head;
        else
            // 取下一个节点或最后一个节点
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

没有加锁,都是CAS操作,方法核心是for循环,循环没有出口直到尝试成功,这也是CAS操作流程。

在插入第二个节点时,t还在head位置上,因此p.next指向实际的第一个元素,因此第6行的q!=null表示q不是最后的节点。为了找到最后一个节点程序进入23行来获得最后一个节点,然后p更新自己的next为新节点,如果成功,p!=t成功,则会更新p的位置将其移动到链表末尾。

第17行用来处理哨兵节点,就是指向自己的节点,这种节点要删除。

第20行更奇怪了

p = (t != (t = tail)) ? t : head;

首先!=操作不是原子操作,也就是说,执行!=时,程序会先取得t的值,再执行t!=tail,然后比较这两个t,但线程情况下t!=t的情况不会发生,但多线程就不一定了。

5.BlockingQueue

我们通常用BlockingQueue来作为线程通信的中介,实现线程A通知线程B,又希望线程A不知道线程B的存在,这样做一个解耦。

与前面几个不同的是,BlockingQueue是一个接口而不是一个实现,它有两个基本实现:ArrayBlockingQueue和LinkedBlockingQueue。

ArrayBlockingQueue是数组实现的,适合做有界队列,因为队列中可容纳的最大元素要在队列创建时指定(数组动态扩张不方便)。LinkedBlockingQueue适合做无界队列,或边界值非常大的序列,因为内部元素可以动态增加。

BlockingQueue为什么适合作为数据共享的通道?
关键在于Blocking(阻塞)。当服务线程处理完队列的所有消息后,他如何知道下一条消息何时到来呢?

一种简单的做法就是让这个线程按一定的时间间隔不停地循环和监控这个队列,这样虽然可行但是造成了不必要的资源浪费,而且循环周期难以确定,BlockingQueue很好地解决了这个问题。它会让服务线程在队列为空时进行等待,当有新消息进入队列后,自动将线程唤醒。

那它是怎么实现的呢?我们以ArrayBlockingQueue为例来看一下。

ArrayBlockingQueue的内部元素都放在一个对象数组中:

final Object items;

向队列压入元素可以使用offer()或put(),offer插入失败后会返回false,put插入失败后会等待。
从队列弹出元素可以使用poll()或take(),如果队列为空,poll()会返回null,take()会等待。

所以put()和take()才是体现Blocking的关键。为了做好等待和通知,ArrayBlockingQueue定义了一些字段:

/** 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是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

构造方法:

public ArrayBlockingQueue(int capacity, boolean fair) {
	if (capacity <= 0)
		throw new IllegalArgumentException();
	this.items = new Object[capacity];
	lock = new ReentrantLock(fair);
	notEmpty = lock.newCondition();
	notFull =  lock.newCondition();
}

take方法:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await(); // 等待
        return dequeue(); // 出队
    } finally {
        lock.unlock();
    }
}

当执行take操作时,如果队列为空,则让当前线程在notEmpty上等待。新元素入队时,则进行一次notEmpty上的通知。看下入队函数:

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

最后一行notEmpty.signal();通知了等待在notEmpty上的线程,让它们继续工作。

同理,对于put()方法的操作也是一样的,当队列满时,需要让压入线程等待,如下第7行所示:

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

当有元素出队时,队伍出现空位,自然也需要通知等待入队的线程

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

从实现上说,ArrayBlockingQueue类在物理上是一个数组,但在逻辑层面是一个环形结构。由于数组大小在初始化时就已经指定,当有元素加入或离开ArrayBlockingQueue时,总是使用 takeIndexputIndex两个变量分别表示队列头部和尾部元素在数组中的位置,每一次出入队都会调整这两个元素的位置。

BlockingQueue使用非常普遍,尤其是在大名鼎鼎的生产者-消费者模式中:生产者与消费者模型

6.ConcurrentSkipListMap

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。

普通链表:
在这里插入图片描述
跳表:
Java 并发容器_第1张图片
比如,我们想查找23,查找的路径是沿着上图中标红的指针所指向的方向进行的。

跳表和平衡树有些类似,但是对平衡树的插入和删除可能会导致平衡树进行一次全局调整,而跳表只需要进行一次局部调整。这样的好处是:在高并发情况下,你会需要一个全局锁来保证整个平衡树的线程安全,而对于跳表只需要一个局部锁,这样就能有更好的性能。

使用跳表实现Map和使用哈希算法实现Map的另外一个不同之处是:哈希并不保存元素的顺序,而跳表内所有元素都是有序的。

实现这一结构的是ConcurrentSkipListMap。

节点:

static final class Node<K,V> {
	final K key;
	volatile Object value;
	volatile Node<K,V> next;

对Node的所有操作采用CAS方法。

另一个重要的数据结构就是Index,这个表示索引内部包装了Node,同时增加了向下的引用和向右的引用。

static class Index<K,V> {
	final Node<K,V> node;
	final Index<K,V> down;
	volatile Index<K,V> right;

整个跳表就是根据Index进行全网的组织的。除此之外还有HeadIndex作为表头记录处于哪一层。

你可能感兴趣的:(并发,Java,java,多线程)