准备线程安全

android 多线程 — 并发集合 CopyOnWriteAr

2018-06-04  本文已影响396人  前行的乌龟

本篇我们来看看集合在多线程环境下的新变化

在多线程中我们需要考虑任何数据对象的同步问题,使用率很高的集合类型对象也不例外,更是重中之重。集合是存储容器的,多线程环境下是线程访问的重点,那么就可能造成并发的问题。java 传统的集合中只有 Vector 、HashTable、StringBuffer 是线程安全的,但只能做到 synchronized 的效果,限制集合在同一时间只能游一个线程来操作,对于其他的对象到是没问题,但是上面说过了,集合是访问重点,是会频繁使用的,这样一次只能游一个线程操作,其他的都得在后面等着(阻塞),在并发量大的情况下会产生巨大的性能问题。那么有什么办法吗

当然有啦,要不我怎么会写这篇文章啊,JAVA 提供了可以在多线程环境下使用的新的集合容器,既能保证数据同步,也能提高访问效率

这几个新的集合容器就是:

我找到的比较好的描述:

JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能。因为同步容器将所有对容器状态的访问都

串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低。因此Java5.0开

始针对多线程并发访问设计,提供了并发性能较好的并发容器,引入了java.util.concurrent包。与Vector和Hashtable、

Collections.synchronizedXxx()同步容器等相比,util.concurrent中引入的并发容器主要解决了两个问题:

  1. 根据具体场景进行设计,尽量避免synchronized,提供并发性。
  2. 定义了一些并发安全的复合操作,并且保证并发环境下的迭代操作不会出错。

CopyOnWriteArrayList


CopyOnWriteArrayList 名字看着可能会有点懵逼,见名知意的话是个啥意思。其他很简单的,就是读写分离的 list ,多线程环境下对集合的读操作是不加锁的,允许多个线程同时读取集合内容;对集合写的操作是加锁的。

这样把读和写操作分离,一个线程同步,一个线程不同步,根据线程安全性区分操作,无疑可以大大提高不影响线程安全操作的多线程效率

大家想啊,读操作只是取数据,不会对数据造成影响,天然的是可以允许并发的

写操作是要改变数据的,是会别的线程造成影响的,肯定是要保证线程安全的。

CopyOnWriteArrayList 读写分离的做法体现了多线程优化的一个思路,把关乎线程安全与否的操作分离,会大大提供不影响线程安全的操作的效率。

这里解释下 CopyOnWriteArrayList 的原理,为啥读和写可以分离。CopyOnWriteArrayList 在写操作时,先把集合数据 copy 于一份出来,然后在这个副本上对集合进行操作,计算结速后再把用副本数据覆盖原始数据,写操作是线程安全的,是同步的,同一时刻只能有一个线程操作。在写操作的同时因为我们不直接修改原始数据,而是用的副本,对原始数据没有任何影响,所以读的操作可以不受写操作的干扰,可以并发操作。

这让我想起 realm 数据库来了,realm 就是在每一个线程都存在一个数据库的副本,我们在这个线程中操作的是副本,然后 realm 自己决定何时同步副本数据到主数据库中。也是用的是副本的套路来支持多线程并发的。任何线程的对数据的操作都不影响别的线程

我们来看下 CopyOnWriteArrayList 的写入方法就更清楚了,源码很简单的,不要有压力

    public boolean add(E e) {
        synchronized (lock) {
            // 取出数据
            Object[] elements = getArray();
            int len = elements.length;
            // 创建副本
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            // 用副本复杂原始数据
            setArray(newElements);
            return true;
        }
    }

CopyOnWrite的缺点

ConcurrentHashMap


HashMap 是根据散列值分段存储的,同步 Map 在同步的时候锁住了所有的段,而ConcurrentHashMap 给每个散列值分段都加了一把锁,这样 ConcurrentHashMap 能允许对不同散列值分段的并发操作,当然同散列值分段的操作还是只能有一个线程的,但是这样能大大提高了并发性能

ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:


nuEZ0.png

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

另外 ConcurrentHashMap 也是读写分离的,get() 是不加锁的,put 加锁

最后


阻塞队列其实也算是并发容器的,但是这个我想和线程池一起说。并发集合容器就说到这里了,我也是做 android 的,对于多线程也是个门外汉,没啥实战经验,对基础理解页很浅薄。这里我简单介绍了2种并发容器的概念和原理,剩下的使用其实和集合没却别,更多的内容请大家自己去找更详细的资料吧

参考资料


上一篇 下一篇

猜你喜欢

热点阅读