《Java并发编程实战》课程笔记(十三)

并发容器

同步容器及其注意事项

  • Java 中的容器主要可以分为四个大类,分别是 List、Map、Set 和 Queue,但并不是所有的 Java 容器都是线程安全的。
    • 例如,我们常用的 ArrayList、HashMap 就不是线程安全的。
    • 如何将非线程安全的容器变成线程安全的容器?只要把非线程安全的容器封装在对象内部,然后控制好访问路径就可以了。
  • 组合操作需要注意竞态条件问题,即便每个操作都能保证原子性,也并不能保证组合操作的原子性。
    • 在容器领域一个容易被忽视的“坑”是用迭代器遍历容器。
    • 这些经过包装后线程安全容器,都是基于 synchronized 这个同步关键字实现的,所以也被称为同步容器。
    • Java 提供的同步容器还有 Vector、Stack 和 Hashtable,这三个容器不是基于包装类实现的,但同样是基于 synchronized 实现的,对这三个容器的遍历,同样要加锁保证互斥。

并发容器及其注意事项

  • Java 在 1.5 版本之前所谓的线程安全的容器,主要指的就是同步容器。
  • 不过同步容器有个最大的问题,那就是性能差,所有方法都用 synchronized 来保证互斥,串行度太高了。
  • 因此 Java 在 1.5 及之后版本提供了性能更高的容器,我们一般称为并发容器。
    《Java并发编程实战》课程笔记(十三)_第1张图片

List

  • List 里面只有⼀个实现类就是 CopyOnWriteArrayList。
    • CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。
    • CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于 array 进行的,迭代器 Iterator 遍历的就是 array 数组。
      《Java并发编程实战》课程笔记(十三)_第2张图片
    • 如果在遍历 array 的同时,还有一个写操作,例如增加元素,CopyOnWriteArrayList 是如何处理的呢?
      • CopyOnWriteArrayList 会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组。
      • 读写是可以并行的,遍历操作一直都是基于原 array 执行,而写操作则是基于新 array 进行。
        《Java并发编程实战》课程笔记(十三)_第3张图片
  • 使用 CopyOnWriteArrayList 需要注意的“坑”主要有两个方面。
    • 一个是应用场景,CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。
    • 另一个需要注意的是,CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。

Map

  • Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap。
    • 它们从应用的角度来看,主要区别在于 ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的。
    • 所以如果你需要保证 key 的顺序,就只能使用 ConcurrentSkipListMap。
  • 使用 ConcurrentHashMap 和 ConcurrentSkipListMap 需要注意的地方是,它们的 key 和 value 都不能为空,否则会抛出 NullPointerException 这个运行时异常。
    《Java并发编程实战》课程笔记(十三)_第4张图片
  • ConcurrentSkipListMap 的 SkipList 本身就是⼀种数据结构跳表。
    • 跳表插⼊、删除、查询操作平均的时间复杂度是 O(logn),理论上和并发线程数没有关系。
    • 所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试⼀下 ConcurrentSkipListMap。

Set

  • Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,使用场景可以参考 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它们的原理都是一样的。

Queue

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

  • 单端阻塞队列
    《Java并发编程实战》课程笔记(十三)_第5张图片

    • 其实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。
    • 内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是 LinkedBlockingQueue);
    • 甚至还可以不持有队列(其实现是 SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。
    • LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;
    • PriorityBlockingQueue 支持按照优先级出队;
    • DelayQueue 支持延时出队。
  • 双端阻塞队列
    《Java并发编程实战》课程笔记(十三)_第6张图片

    • 其实现是 LinkedBlockingDeque。
  • 单端非阻塞队列

    • 其实现是 ConcurrentLinkedQueue。
  • 双端非阻塞队列

    • 其实现是 ConcurrentLinkedDeque。
  • 使用队列时,需要格外注意队列是否支持有界(所谓有界指的是内部的队列是否有容量限制)。

    • 实际工作中,一般都不建议使用无界的队列,因为数据量大了之后很容易导致 OOM。
    • 只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。

你可能感兴趣的:(Java,基础,java,笔记,jvm)