Java并发20-并发容器中的坑


Java并发20-并发容器中的坑

Java中的容器主要可分为四个大类,分别是ListMapSetQueue

如何将非线程安全容器变成线程安全—Java并发12:把非线程安全容器封装在对象内部,然后控制好访问路径。

SafeArrayList<T>{
  //封装ArrayList
  List<T> c = new ArrayList<>();
  //控制访问路径
  synchronized T get(int idx){
    return c.get(idx);
  }

  synchronized void add(int idx, T t) {
    c.add(idx, t);
  }

  synchronized boolean addIfNotExist(T t){
    if(!c.contains(t)) {
      c.add(t);
      return true;
    }
    return false;
  }
}

JDK做了类似的包装:

List list = Collections.synchronizedList(new ArrayList());
Set set = Collections.synchronizedSet(new HashSet());
Map map = Collections.synchronizedMap(new HashMap());

注意竞态条件

切记:组合操作需要注意竞态条件问题

List list = Collections.synchronizedList(new ArrayList());
Iterator i = list.iterator(); 
//这里就产生了竞态条件 执行5行的时候 可能已经无next()了。
while (i.hasNext())
  foo(i.next());

正确做法:

List list = Collections.synchronizedList(new ArrayList());
synchronized (list) {  
  Iterator i = list.iterator(); 
  while (i.hasNext())
    foo(i.next());
}    

SychronizedListSychronizedMapSychronizedSetVectorStackHashTable都是sychronized关键字实现的,称为同步容器

VectorStackHashtable这三个容器不是基于包装类实现的,对这三个容器的遍历,同样要加锁保证互斥。

并发容器

并发容器

JDK1.5之前主要指同步容器线程安全,但性能差。1.5之后提供了性能更好的并发容器,并发容器遍历线程安全。

List

List 里面只有一个实现类就是CopyOnWriteArrayList

CopyOnWriteArrayList内部维护了一个数组,成员变量array就指向这个内部数组,所有的读操作都是基于 array进行的,如下图所示,迭代器Iterator遍历的就是array数组。

执行迭代的内部数组

如果遍历array时,还有一个写操作,例如增加元素,CopyOnWriteArrayList会将array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将array指向新的数组。

Map

Map接口的两个实现是ConcurrentHashMapConcurrentSkipListMap

ConcurrentHashMapkey无序,ConcurrentSkipListMapkey有序。

它们的keyvalue都不能为空

各Map对Key&Value的要求

ConcurrentSkipListMap里面的SkipList本身就是一种数据结构。

跳表插入、删除、查询操作平均的时间复杂度是O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若ConcurrentHashMap的性能还不够满意,可以尝试ConcurrentSkipListMap

Set

Set接口的两个实现是CopyOnWriteArraySetConcurrentSkipListSet,使用场景可以参考前面讲述的 CopyOnWriteArrayListConcurrentSkipListMap,它们的原理都是一样的。

Queue

阻塞与非阻塞:当队列已满,入队操作阻塞;当队列已空时,出队操作阻塞。

单端与双端:单端即队尾入队,队首出队;双端即队首队尾皆可入队出队。

Java并发包里阻塞队列都用Blocking关键字标识,单端队列使用Queue标识,双端队列使用Deque标识。

  • 单端阻塞队列

    ArrayBlockingQueue

    内部持有一个数组队列。

    LinkedBlockingQueue

    内部持有一个链表队列。

    SynchronousQueue

    内部不持有队列,生产者-消费者模式下,生产者线程的入对操作必须等待消费者线程的出队操作。

    LinkedTransferQueue(大佬工作中没用过)

    结合了LinkedBlockingQueue&SynchronousQueue功能,性能比LinkedBlockingQueue好。

    PriorityBlockingQueue

    支持按照优先级出队。

    DelayQueue

    支持延时出队。

    单端阻塞队列

  • 双端阻塞队列

    LinkedBlockingDeque

    双端阻塞队列

  • 单端非阻塞队列

    ConcurrentLinkedQueue

  • 双端非阻塞队列

    ConcurrentLinkedDeque

实际工作中,一般都不建议使用无界的队列(即没有设置队列容量),因为数据量大了之后很容易导致 OOM。

只有ArrayBlockingQueueLinkedBlockingQueue是支持有界的。

要学会根据容器特性选择才是关键。

思考

线上系统 CPU 突然飙升,怀疑在并发场景里使用了HashMap,因为在1.8之前的版本里并发执行HashMap.put()可能会导致CPU飙升到100%,该如何验证猜测?

Java7中的HashMap在执行put操作时会涉及到扩容,由于扩容时链表并发操作会造成链表成环,所以可能导致cpu飙升100%。

——黑白尤文

为何concurrentskiplistmapconcurrenthashmap性能好?

如果key冲突比较大,hashmap还是要靠链表或者tree来解决冲突的,所以O(1)是理想值。

同时增删改操作很多也影响hashmap性能。这个也是要看冲突情况,也就是说hashmap的稳定性差。

如果很不幸正好偶遇它的稳定性问题,同时又接受不了,就可以尝试skiplistmap,它能保证稳定性,无论并发量是多大,也没有key冲突的问题。


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录