【java并发工具类-协作】并发容器


java中容器主要分为四大类:List,Map,Set,和Queue,但并不是所有的容器都是线程安全的,比如ArrayList,就不是线程安全的,

1.那么如何可以把ArrayList变成线程安全的容器呢?

其实,思路很简单,只要把非线程安全的容器封装在对象内部,然后控制好访问方法即可。

下面我们就以 ArrayList 为例,看看如何将它变成线程安全的。

SafeArrayList<T>{
  List<T> c = new ArrayList<>(); //封装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;
  }
}

上面封装ArrayList,然后用synchronized锁住访问ArrayList方法,即只能一个线程访问ArrayList,就变成线程安全的了,那么所有的非线程安全的容器不都可以这样实现线程安全么?

2. 同步容器

java SDK想到上面那种情况,提供了该方法。

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(); 
while (i.hasNext())
  foo(i.next());

虽然容器是线程安全的,访问单个方法没有问题,但是组合起来就存在安全问题了,当线程T1访问i.hasNext(),线程T2访问i.next(),那么线程T1在访问i.next()就存在问题了。
正确做法:

List list = Collections. synchronizedList(new ArrayList());
synchronized (list) {  //加锁之后只能一个线程访问这两个组合操作了
  Iterator i = list.iterator(); 
  while (i.hasNext())
    foo(i.next());
}    

Java 提供的同步容器还有 Vector、Stack 和 Hashtable,同样是基于 synchronized 实现的,对这三个容器的遍历,同样要加锁保证互斥。

3. 并发容器

java1.5版本之前的所谓的线程安全的容器指的是同步容器。因为是synchronized实现保证互斥,串行化太高,性能太差。
之后 java 1.5版本之后提供了性能更高的容器,一般称为并发容器

并发容器依然是那四大类:
在这里插入图片描述

3.1 List

List 里面只有一个实现类就是 CopyOnWriteArrayList。CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。

  • 结合下图,理解CopyOnWriteArrayList的原理。
    CopyOnWriteArrayList内部维护了一个数组,成员变量array指向数组,,所有的读操作都是基于这个array进行的。如果在遍历的同时,还有一个写操作,例如增加元素9,,它会将原数组复制一份,然后对copy的数组写操作, 执行完,将array指向copy的数组。
    在这里插入图片描述

  • 应用场景:CopyOnWriteArrayList仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。比如上面的操作,写入的元素并不能被立即遍历到。

  • 注意:CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

3.2 Map

Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap。

  • 区别: ConcurrentHashMap 的key是无序的。 ConcurrentSkipListMap 的 key 是有序的。
  • 下面这个表格总结了 Map 相关的实现类对于 key 和 value 的要求。
    在这里插入图片描述
  • 性能:ConcurrentSkipListMap 里面的 SkipList 本身就是一种数据结构,中文一般都翻译为“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。

3.3 Set

Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,

  • 使用场景可以参考前面讲述的 CopyOnWriteArrayList(读多写少) 和 ConcurrentSkipListMap(key有序),它们的原理都是一样的,这里就不再赘述了。

3.4 Queue

java并发包中队列可以分为两个维度来分类:

  • 阻塞和非阻塞:阻塞是指队列满了,入队操作阻塞,队列空是,出队操作阻塞。Java并发包中用Blocking关键字表示阻塞队列。
  • 单端和双端:单端只能队尾入队,队首出队;双端是队尾和队首都可以入队出队。单端队列用Queue标识,双端队列用Deque标识

两个维度组合后分为四大类:

  • 单端阻塞队列:实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
  1. ArrayBlockingQueue:内部一般会持有一个数组队列。
  2. LinkedBlockingQueue:内部一般会持有一个链表队列。
  3. SynchronousQueue:不持有队列,生产者消费者模式,生产者的线程入队操作必须等待消费者线程的出队操作。
  4. LinkedTransferQueue:融合LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue好。
  5. PriorityBlockingQueue 支持按照优先级出队。
  6. DelayQueue 支持延时出队。
  • 双端阻塞队列:其实现是 LinkedBlockingDeque。
    在这里插入图片描述
  • 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
  • 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。

注意:上面这些队列中只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。
一般工作中不建议使用无界的队列。

参考:极客时间
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105173018