算法与数据结构(3),并发结构

算法与数据结构(1),List

算法与数据结构(2),Map

算法与数据结构(3),并发结构

本来已经合上电脑了,躺在床上,翻来覆去睡不着,索性,不睡了,起床,听听歌,更新简书,这可能是这一系列的最后一篇,脚趾的伤也好的差不多了,下个礼拜就要全身心找工作了。

并发List

Vector和CopyOnWriteArrayList是两个线程安全的List实现ArrayList不是线程安全的。因此,应该尽量避免在多线程环境中使用ArrayList。如果因为某些原因必须,则需要使用Collections.synchronizedList( )进行包装。

CopyOnWriteArrayList的内部实现与Vector不同,从字面中可以看出Copy-On-Write就是CopyOnWriteArrayList的实现机制。即当对象进行写操作时,复制该对象;若进行的时读操作,则直接返回结果,操作过程中不进行同步。

CopyOnWriteArrayList很好利用了对象的不变性,在没有对象进行写操作之前由于对象未发生改变,因此不需要加锁。而在视图改变对象时,总是先获取对象的一个副本,然后对副本进行修改,最后将副本写回。

这种实现方式的核心思想是减少锁竞争,从而提高并发时的读取性能,但是它却一定程度上牺牲了写的性能。

get( )方法如下:

/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;//内置数组被关键字volatile修饰

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

/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}

可以看到,作为一个线程安全的实现,CopyOnWriteArrayList的get( )没有任何锁操作,而对比Vector的get( )实现:

/**
 * Returns the element at the specified position in this Vector.
 *
 * @param index index of the element to return
 * @return object at the specified index
 * @throws ArrayIndexOutOfBoundsException if the index is out of range
 *            ({@code index < 0 || index >= size()})
 * @since 1.2
 */
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

Vector使用了同步关键字synchronized所有的get( )操作都必须先等待对象锁的释放,才能进行。在高并发的情况下,大量的锁竞争会降低系统性能。

虽然CopyOnWriteArrayList的读操作性能优越,但是,基于CopyOnWriteArrayList的写操作却不能尽如人意。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#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();
    }
}

在每一次add( )方法中,CopyOnWriteArrayList都进行一次自我复制,同时add( )操作也申请了锁,并不像get( )那样。相对的,Vector的add( )方法则要快捷的多。

/**
 * Appends the specified element to the end of this Vector.
 *
 * @param e element to be appended to this Vector
 * @return {@code true} (as specified by {@link Collection#add})
 * @since 1.2
 */
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);     //内置数组是否需要扩容
    elementData[elementCount++] = e;
    return true;
}

因此,在高并发且以读为主的应用场景中,CopyOnWriteArrayList要优于Vector。但是当写操作很频繁时,CopyOnWriteArrayList的效率并不高,可以考虑优先使用Vector。

并发Map
在多线程环境中使用Map,一般也可以使用Collections.synchronizedMap( )进行包装。

但是在高并发情况下,这个Map的性能表示不是最优的。因为被包装后的Map,在进行读写操作时都要等待锁的释放。

在高并发的环境中,可以使用ConcurrentHashMap,写操作的效率比同步HashMap快了将近一倍,ConcurrentHashMap之所以有如此之高的吞吐量,得益于其内部实现了锁桶的锁分离机制,在读写整张Entry数组表的时候,不需要像HashMap那样锁住整张表,而是只锁当前需要用到的桶,原来只能一个线程进入,现在却能同时16(默认16个桶)个写线程进入,并发性的提升是显而易见的。同时,ConcurrentHashMap的get( )操作是无锁的。这些都为ConcurrentHashMap在多线程并发下的高性能提供了保证。

ConcurrentHashMap是专门为线程设计的HashMap。它的get( )操作时无锁的,它的put( )操作的锁粒度又小于同步HashMap。因此它的整体性能优于同步的HashMap。

并发Queue

Queue是一种特殊的线性结构队列,只允许从队列的头部移除元素,或者从队列的尾端添加元素,以一种FIFO(先进先出)的方式管理数据。

add( ),和remove( )方法。

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");      //队列已满,抛出异常
}

public E remove() {
    E x = poll();
    if (x != null)
        return x;
    else
        throw new NoSuchElementException();     //队列为空,抛出异常
}

由此可见,应该尽量避免使用add( ),和remove( )方法。而使用offer( )来加入元素,使用poll( )来获取并移出元素。

并发队列,有两种实现,一个是以ConcurrentLinkedQueue为代表的高性能队列,一个是以BlockingQueue为代表的阻塞队列。

ConcurrentLinkedQueue是一个适用于高并发场景下的队列。它通过无锁方式,实现了高并发状态下的高性能。

与ConcurrentLinkedQueue相比BlockingQueue的主要功能不是在于提升高并发时的队列功能,而在于简化多线程间的数据共享。

BlockingQueue的典型使用场景是生产-消费者模式中,生产者总是将产品放入BlockingQueue队列中,而消费者从队列中取出产品消费,从而实现数据共享。

BlockingQueue提供一种读写阻塞等待的机制,即如果消费者速度过快,则BlockingQueue可能被清空,此时,消费线程再试图从BlockingQueue读取数据时就会被阻塞。反之,如果生产线程过快,则BlockingQueue可能会被装满,此时,生产线程再试图向BlockingQueue队列中装入数据时,便会阻塞等待。

《算法与数据结构(3),并发结构》 BlockingQueue的工作模式

BlockingQueue提供了两种主要实现:

  1. ArrayBlockingQueue:它是一种基于数组的阻塞队列实现,在ArrayBlockingQueue内部还维护了一个定长的数组,用于缓存队列中的数据对象。此外,ArrayBlockingQueue内部还存着两个整型变量,分别标识着队列头部和尾部在数组中的位置。

  2. LinkedBlockingQueue:这是一个基于链表的阻塞队列,ArrayBlockingQueue类似,内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时,才会阻塞生产者队列,直到消费者从队列中消费掉一个数据,生产者线程才能被唤醒。

并发Deque

Deque是一种双端队列,允许在队列的头部或者尾部进行出队和入队操作。

由于Deque这个接口日常工作中很少用到,这里只做简单介绍。

LinkedList,ArrayDeque和LinkedBlockingDeque都实现了Deque接口。其中,LinkedList使用链表实现了双端队列,ArrayDeque使用数组实现了双端队列。通常情况下ArrayDeque是基于数组实现的,所以拥有高效的随机访问性能,因此ArrayDeque具有更好的遍历性。但是当队列大小变化较大时,ArrayDeque需要重新分配内存并进行数组复制,在这种情况下,基于链表的LinkedList没有内存调整和数组复制的负担,性能表现会较好。但是,无论,ArrayDeque还是LinkedList,他们都不是线程安全的。

在AsyncTask的源代码中

private static class SerialExecutor implements Executor {

    final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
    Runnable mActive;
    
    /*ArrayDeque不是线程安全的,execute需要用关键字synchronized 修饰*/
    public synchronized void execute(final Runnable r) {
        mTasks.offer(new Runnable() {
            public void run() {
                try {
                    r.run();
                } finally {
                    scheduleNext();
                }
            }
        });
        if (mActive == null) {
            scheduleNext();
        }
    }
    protected synchronized void scheduleNext() {
        if ((mActive = mTasks.poll()) != null) {
            THREAD_POOL_EXECUTOR.execute(mActive);
        }
    }
}

LinkedBlockingDeque是一个线程安全的双端队列。在内部是现中,LinkedBlockingDeque使用链表结构。每一个队列节点都维护一个前驱节点和一个后驱节点。LinkedBlockingDeque并没有进行读写锁的分离,因此同一时间只能有一个线程对其进行访问。因此,在高并发应用中,它的性能表现要远低于LinkedBlockingQueue,更低于ConcurrentLinkedQueue。

片尾TIP:

private SparseArray<String> sparseArray = new SparseArray<String>();
private SparseIntArray sparseIntArray = new SparseIntArray();
private SparseBooleanArray sparseBooleanArray = new SparseBooleanArray();
private LongSparseArray<String> longSparseArray = new LongSparseArray<String>();

public void Test() {

    sparseArray.put(1, "1");
    sparseIntArray.put(2, 2);
    sparseBooleanArray.put(3, true);
    longSparseArray.put(4, "4");
}

使用优化后的数据集合,可以避免掉基本数据类型转换成对象数据类型时浪费的时间。

数据结构这个系列,暂且告一段落,最后,我想把这段话送给大家。

送给大家的话

    原文作者:小鄧子
    原文地址: https://www.jianshu.com/p/92905d10cdfc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞