­

那些年应该相识的线程安全集合们

  • 2019 年 10 月 6 日
  • 筆記

上篇推文介绍了List的三种实现其实都不是线程安全的,文章结尾也回答了如何创建线程安全的List,答案是:Collections.synchronizedList

接下来小强再后台收到热心童鞋(在此特别感谢并艾特三老师)回复了其他方式List线程安全的实现方式:CopyOnWriteArrayList

本文就为大家总结下Concurrent下常用的线程安全集合们。主要包含以下两种类型的集合:

– CopyOnWriteArrayList

– BlockingQueue/BlockingDeque

CopyOnWriteArryList

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的:

    public void add(int index, E element) {         final ReentrantLock lock = this.lock;         lock.lock();          try {             Object[] elements = getArray();             int len = elements.length;              if (index > len || index < 0)                  throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);             Object[] newElements;             int numMoved = len - index;              if (numMoved == 0)                  newElements = Arrays.copyOf(elements, len + 1);              else {                  newElements = new Object[len + 1];                 System.arraycopy(elements, 0, newElements, 0, index);                 System.arraycopy(elements, index, newElements, index + 1,                                  numMoved);             }              newElements[index] = element;             setArray(newElements);         } finally {             lock.unlock();         }     }

传统的List在多线程同时读写的时候会抛出java.util.ConcurrentModificationException,而CopyOnWriteArrayList是使用CopyOnWrite(写时复制)技术解决了这个问题,上面代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更有效。

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

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

所以,通过上面的介绍,CopyOnWriteArrayList并发场景更适合于读多写少的场景。

BlokingQueue/BlockingDeque

阻塞队列BlockingQueue是一个接口,BlockingDeque继承了BlockingQueue,代码如下:

public interface BlockingQueue<E> extends Queue<E> {  }
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {  }

这里两个比较相似的阻塞队列,BlockingQueue和BlockingDeque,两个都是队列,只不过前者只能一端出一端入,后者则可以两端同时出入,并且他们的实现类都是结构改变线程安全的队列。

BlockingQueue接口的常用实现有以下几种:

  • ArrayBlockingQueue
  • DelayQueue
  • LinkedBlockingQueue
  • PriorityBlockingQueue

BlockingDeque接口的常用实现有以下几种:

  • LinkedBlockingDeque

其实两个队列从实现思想上比较容易理解,有以下特点:

  • 链表结构(动态数组)
  • 通过ReentrantLock实现锁
  • 利用Condition实现队列的阻塞等待,唤醒

下面以ArrayBlockingQueue的构造方法举例来了解以上特点:

    public ArrayBlockingQueue(int capacity, boolean fair) {          if (capacity <= 0)              throw new IllegalArgumentException();          this.items = new Object[capacity];         lock = new ReentrantLock(fair);         notEmpty = lock.newCondition();         notFull =  lock.newCondition();     }

结语

其实还有一种很常见的线程安全结合ConcurrentHashMap。它是一种支持高并发、高吞吐量的线程安全HashMap实现,其中,在java8中对ConcurrentHashMap的结构进行了很大的改造。

以后的文章,小强可以详细介绍这部分内容,因为这部分属于JAVA面试中非常常见的问题,需要专门的篇幅来介绍。在此不赘述。