JAVA并发体系-3-并发容器

Copy-On-Write(COW)

Copy-On-Write简称COW(不会产奶的奶牛),是一种用于程序设计中的优化策略。Copy On Write技术在Linux和文件系统中均有应用,Linux通过Copy On Write技术极大地减少了Fork的开销,文件系统通过Copy On Write技术一定程度上保证数据的完整性。(todo: 专门写一篇文章去阐述这个事情

而在Java的concurrent包中也有他的身影,本节就是阐述concurrent包中的Copy On Write技术。

Copy-On-Write的基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略

concurrent包中含有依据COW的组件:CopyOnWriteArrayList和CopyOnWriteArraySet

什么是CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

  • 添加元素的过程:

    通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素(添加过程需要加锁),添加完元素之后,再将原容器的引用指向新的容器。

  • 优点:

    可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

  • 缺点:

    • 内存占用问题

      在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。如果这些对象占用的内存比较大,则会占用比较多的资源,造成长时间GC或频繁的GC。

      压缩容器中的元素或使用其他并发容器,如ConcurrentHashMap

    • 数据一致性问题

      只能保证数据的最终一致性,不能保证数据的实时一致性

  • 应用场景:

    读多写少的并发场景。例子比如:

    • 白名单,黑名单
    • 商品类目的访问和更新场景
  • 类库组件:CopyOnWriteArrayList和CopyOnWriteArraySet

    CopyOnWriteArrayList和CopyOnWriteArraySet分别实现了List接口和AbstractSet接口,其中CopyOnWriteArraySet含有一个CopyOnWriteArrayList实例域,进而实现COW应用。因此它们两个行为的类似

实现原理(读/写)

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        ......
        Object[] newElements = Arrays.copyOf(current, len + 1);// 复制出新数组
        newElements[len] = e; // 把新元素添加到新数组里
        setArray(newElements);// 把原数组引用指向新数组
        return true;
    } finally {
        lock.unlock();
    }
}
...
final void setArray(Object[] a) {
    array = a;//直接修改引用
}

以上代码是写元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

读元素的时候需要加锁,如果读的时候有多个线程正在向容器添加数据,还是会读到旧数据,因为写的时候不会锁住旧的容器

ConcurrentMap

todo: ConcurrentHashMap

todo: ConcurrentSkipListMap ==> ConcurrentSkipListSet

todo: 聊聊并发(四)深入分析ConcurrentHashMap

多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap

效率低下的HashTable容器, HashTable容器使用synchronized来保证线程安全,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态

ConcurrentHashMap的锁分段技术

ConcurrentHashMap的初始化

定位Segment

ConcurrentHashMap的get操作

ConcurrentHashMap的size操作

ConcurrentLinkedQueue

todo: ConcurrentLinkedQueue

todo: ConcurrentLinkedDeque

实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法。

使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,ConcurrentLinkedQueue是使用非阻塞的方式来实现线程安全队列的

BlockingQueue

todo: BlockingQueue

todo: 聊聊并发(七)——Java中的阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。(即生产和消费

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

提供了7个阻塞队列。分别是

  • ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
  • PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列。
    • 队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口
    • 可以用于:
      • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
      • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
  • SynchronousQueue:一个不存储元素的阻塞队列。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

阻塞队列的实现原理

使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。即使用await、signal实现

todo: 聊聊并发(七)——Java中的阻塞队列

参考

  1. 聊聊并发-Java中的Copy-On-Write容器
posted @ 2020-03-23 00:53  cheaptalk肥皂  阅读(140)  评论(0编辑  收藏  举报