【Java编程的逻辑】并发容器

写时复制的List和Set

CopyOnWriteArrayList和CopyOnWriteArraySet,Copy-On-Write即写时复制

CopyOnWriteArrayList

CopyOnWriteArrayList实现了List接口,它的用法与其他的List基本是一样的。
CopyOnWriteArrayList特点:
1. 线程安全,可以被多个线程并发访问
2. 迭代器不支持修改操作,但不会抛出ConcurrentModificationException
3. 以原子方式支持一些符合操作

基于synchronized的同步容器中有几个问题。迭代时,需要对整个列表对象加锁;复合操作不安全;伪同步。
而CopyOnWirteArrayList直接支持两个原子方法:

// 不存在才添加,如果添加了,返回true,否则返回false
public boolean addIfAbsent(E e)
// 批量添加c中的非重复元素,不存在才添加,返回实际添加的个数
public int addAllAbsent(Collection<? extedns E> c)

CopyOnWirteArrayList的内部也是 一个数组,但这个数组是以原子方式被整体更新的。每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部的数组引用。

所有的读操作,都是先拿到当前引用的数组,然后直接访问该数组。在读的过程中,可能内部的数组引用已经被修改了,但不会影响读操作,它依旧访问原数组内容。

也就是说:数组内容是只读的,写操作都是通过新建数组,然后原子性地修改数组引用来实现的。


在CopyOnWirteArrayList中,读不需要锁,可以并行,读和写也可以并行,但多个线程不能同时写,每个写操作都需要先获取锁。CopyOnWirteArrayList内部使用ReentrantLock

// 声明了volatile,以保证内存可见性
private volatile transient Object[] array;
final Object[] getArray() {
    return array;
}
final void setArray(Object[] a) {
    arr = a;
}
transient final ReentrantLock lock = new ReentrantLock();
// 构造方法
public CopyOnWirteArrayList() {
    setArray(new Object[0]);
}

add方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取当前数组
        Object[] elements = getArray();
        int len = elements.length;
        // 复制出一个长度加1的新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新数组中添加元素
        newElements[len] = e;
        // 原子性地修改内部数组引用
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

indexOf方法

public int indexOf(E e, int index) {
    // 获取当前数组
    Object[] elements = getArray();
    return indexOf(e, elements, index, elements.length);
}
// 数据都是以参数形式传递,数组内容不会被修改,不存在并发问题
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;
}

CopyOnWriteArrayList的性能很低,不适用于数组很大且修改频繁的场景。它是以优化读操作为目的的,读不需要同步,性能很高,但在优化读的同时牺牲了写的性能

之前介绍过保证线程安全的两种思路:一种是锁,使用synchronized或ReentrantLock;另外一种是循环CAS。 写时复制体现了保证线程安全的一种新思路。
锁和循环CAS都是控制对同一个资源的访问冲突,而写时复制通过复制资源减少冲突。

CopyOnWriteArraySet

CopyOnWriteArraySet实现了Set接口,不包含重复的元素。 内部是通过CopyOnWriteArrayList实现的

add方法就是调用了CopyOnWriteArrayList的addIfAbsent方法。

CopyOnWriteArrayList和CopyOnWriteArraySet适用于读远多于写、集合不太大的场景。

ConcurrentHashMap

ConcurrentHashMap它是HashMap的并发版本,具有以下特点:

  • 并发安全
  • 直接支持一些原子复合操作
  • 支持高并发,读操作完全并行,写操作支持一定程度的并行
  • 与同步容器Collections.synchronizedMap相比,迭代不用加锁,不会抛出ConcurrentModificationException
  • 弱一致性

同步容器使用synchronized,所有方法竞争同一个锁;
ConcurrentHashMap采用分段锁技术,将数据分为多个段,而每个段有一个独立的锁。

弱一致性:ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反应出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来。

基于跳表的Map和Set

Java并发包中与TreeMap/TreeSet对应的并发版本是ConcurrentSkipListMap和ConcurrentSkipListSet。

TreeSet是基于TreeMap实现的,类似地,ConcurrentSkipListSet也是以及ConcurrentSkipListMap实现的。

ConcurrentSkipListMap是基于SkipList实现的,SkipList称为跳跃表或跳表,是一种数据结构。

ConcurrentSkipListMap有如下特点:

  • 没有使用锁,所有操作都是无阻塞的
  • 与ConcurrentHashMap类似,迭代器不会抛出异常,是弱一致性的
  • 与ConcurrentHashMap类似,实现了ConcurrentMap接口,支持一些原子复合操作
  • 与TreeMap类似,可排序,默认按键的自然顺序

ConcurrentSkipListMap的size方法,与大多数容器实现不同,这个方法不是常数量操作,它需要遍历所有元素,时间复杂度是O(N),而且遍历结束后,元素个数可能已经变了。

并发队列

  • 无锁非阻塞并发队列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 普通阻塞队列:基于数组的ArrayBlockingQueue,基于链表的LinkedBlockingQueue和LinkedBlockingDeque
  • 优先级阻塞队列:PriorityBlockingQueue
  • 延时阻塞队列:DelayQueue
  • 其他阻塞队列:SynchronousQueue和LinkedTransferQueue

无锁非阻塞是指,这些队列不实用锁,所有操作总是立即执行,主要通过循环CAS实现并发安全;
阻塞队列是指,这些队列使用锁和条件,很多操作都需要先获取锁或满足特点条件,获取不到锁或等待 条件时,会等待(阻塞),直到获取到锁或条件满足

无锁非阻塞并发队列

ConcurrentLinkedQueue和ConcurrentLinkedDeque ,它们适用于多个线程并发使用一个队列的场合,都是基于链表实现,没有限制大小,与ConcurrentSkipListMap类似,它们的size方法不是一个常数量运算

ConcurrentLinkedQueue实现了Queue接口,表示一个FIFO队列,从尾部入队,从头部出队,内部是一个单表链表。
ConcurrentLinkedDeque实现了Deque接口,表示一个双端队列,在两端都可以入队和出对,内部是一个双向链表。

两个类最基础的原理都是循环CAS

普通阻塞队列

阻塞队列都实现了接口BlockingQueue,在入队/出队时可能等待,主要方法

// 入队,如果队列满,等待直到队列有空间
void put(E e) throws InterruptedException; 
// 出队,如果队列空,等待直接队列不为空,返回头部元素  
E take() throws InterruptedException; 
// 入队,如果队列满,最多等待指定的时间,如果超时还是满,返回false
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 
// 出队,如果队列空,最多等待指定的时间,如果超时还是空,返回null
E poll(long timeout, TimeUnit unit) throws InterruptedException;  

ArrayBlockingQueue和LinedBlockingQueue都实现了Queue接口;LinedBlockingDequeue实现了Deque接口
ArrayBlockingQueue是基于循环数组实现的,有界,创建时需要指定大小,且在运行过程中
不会改变。与ArrayDeque不同,ArrayDeque也是基于循环数组实现的,但是是无界的,会自动扩展。
LinedBlockingQueue是基于单向链表实现的,在创建时可以指定最大长度,也可以不指定,默认是无限的。LinedBlockingDeque与LinedBlockingQueue一样,最大长度也是在创建时可选的,默认无限,不过它是基于双向链表实现的。

内部它们都是使用显示锁ReentrantLock和显式条件Condition实现的

优先级阻塞队列

普通阻塞队列是先进先出的,而优先级队列是按优先级出队的,优先级高的先出。

PriorityBlockingQueue是PriorityQueue的并发版本

延时阻塞队列

DelayQueue是一种特殊的优先级队列,它是无界的,它要求每个元素都实现Delayd接口

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

getDelay返回一个给定时间单位unit的整数,表示再延迟多长时间,如果小于等于0,则表示不再延迟。

DelayQueue可以用于实现定时任务,它按元素的延时时间出队。只有当元素的延时过期之后才能被从队列拿走。
DelayQueue是基于PriorityQueue实现的,它使用一个锁ReentrantLock保护所有访问

其他阻塞队列

SynchronousQueue与一般队列不同,它没有存储元素的空间。
它的入队操作要等待另一个线程的出队操作,如果没有其他线程在等待从队列中接收元素,put操作就会等待。反之亦然,take操作同理

LinkedTransferQueue实现了TransferQueue接口,TransferQueue是BlockingQueue的子接口,增加了一些额外功能,生产者在往队列中放元素时,可以等待消费者接收后再返回,适用于一些消息传递类型应用中。

猜你喜欢

转载自blog.csdn.net/u013435893/article/details/80165696
今日推荐