集合安卓面试

Android 多线程探索(四)— 同步集合

2018-09-12  本文已影响0人  Little丶Jerry

前言

Android JDK 提供了一系列线程安全的集合,避免多线程环境下由于线程安全导致的各种问题。

一、程序之中的优化策略 — CopyOnWriteArrayList

Copy - On - Write 是一种用于程序设计中的优化策略,其基本思路是:从多个线程共享同一个列表,当某个线程想要修改这个列表的元素时,会把列表中的元素 Copy 一份,然后进行修改,修改完成之后再将新的元素设置给这个列表,这是一种延时懒惰策略。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读、而不需要加锁,因为当前容器不会添加、移除任何元素。所以,CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。

以下代码是向 CopyOnWriteArrayListadd 方法的实现:

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

添加完元素之后,直接让列表的内部数组成员指向新的数组。

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

从上述程序可以发现,在添加的时候进行了加锁操作,否则多线程写的时候会 CopyN 个副本出来。复制一份之后将新的元素设置到元素数组的 len 位置,然后再把最新的元素数组设置给该列表。

读的时候不需要加锁,如果读的时候有多个线程正在向 CopyOnWriteArrayList 添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的元素数组。读的代码如下:

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

通过这种写时拷贝的原理可以将读、写分离,使并发场景下对列表的操作效率得到提高,但它的问题是,在添加、移除元素时占用的内存空间翻了一倍,因此,这是以空间换时间,相似的列表还有 CopyOnWriteArraySet

二、提高并发效率 — ConcurrentHashMap

我们知道,HashTableHashMap 的线程安全实现,但 HashTable 容器使用 synchronized 来保证线程安全,在线程竞争激烈的情况下,HashTable 的效率非常低下。因为,当一个线程访问 HashTable 的同步方法时,其他线程访问 HashTable 的同步方法可能会进入阻塞或轮询状态。如线程 1 使用 put 进行添加元素,线程 2 不但不能使用 put 方法添加元素,并且也不能使用 get 方法来获取元素,所以竞争越激励效率越低。

究其原因是因为,所有访问 HashTable 的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效地提高并发访问效率,这就是 ConcurrentHashMap 所使用的锁分段技术。该技术首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个数据段时,其他段的数据也能被其他线程访问。有些方法需要跨段,如 size()containsValue(),它们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完成之后,又按顺序释放所有段的锁。

三、有效的方法 — BlockingQueue

阻塞队列为用户提供了一些有用的特性,例如,当队列满了时,再次调用 put 函数添加元素,那么调用线程将会阻塞,直到队列不再是填满状态。这个特性很有用,它就是生产者 — 消费者的一个实现,当我们需要这类的功能时直接使用 BlockingQueue 即可,避免了手动判断以及同步操作。

函数名 作用
add(e) 把元素 e 加到 BlockingQueue 里,如果 BlockingQueue 可以容纳,则返回 true,否则抛出异常
offer(e) 把元素 e 加到 BlockingQueue 里,如果 BlockingQueue 可以容纳,则返回 true,否则返回 false
offer(e,time,unit) 把元素 e 加到 BlockingQueue 里,如果 BlockingQueue 可以容纳,则返回 true,否则在等待指定的时间之后继续尝试添加,如果失败则返回 false
put(e) 把元素 e 加到 BlockingQueue 里,如果 BlockingQueue 不能容纳,则调用此方法的线程被阻塞直到 BlockingQueue 里面有空间再继续添加
take() 取走 BlockingQueue 里排在队首的对象,若 BlockingQueue 为空,则进入等待状态直到 BlockingQueue 有新的对象被加入为止
poll(time,unit) 取走并移除队列中队首元素,如果设定的阻塞时间内还没有获得数据,那么返回 null
element() 获取队首元素,如果队列为空,那么抛出 NoSuchElementException 异常
peek() 获取队首元素,如果队列为空,那么返回 null
remove() 获取并移除队首元素,如果队列为空,那么抛出 NoSuchElementException 异常

BlockingQueue 在 Android JDK 中有多个实现:

LinkedBlockingQueue 类似的还有 ConcurrentLinkedQueueConcurrentLinkedQueue 是一个基于链接结点的无界线程安全队列,它采用先进先出的规则对结点进行排序,当添加一个元素时,它会添加到队列的尾部,当获取一个元素时,它会返回队列头部的元素。

这里需要温习一下的就是,双向链表相对于单向链表的优缺点:
上一篇 下一篇

猜你喜欢

热点阅读