只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

47、并发容器

内容来自王争 Java 编程之美

在第 11 节我们讲到,为了方便开发,Java 提供了很多容器,比如 ArrayList、HashMap、TreeSet 等,底层对数组、哈希表、红黑树等常用的数据结构进行了封装
在开发时,我们直接使用这些现成的容器即可,不需要重新从零去开发,对于这些封装了常用数据结构的容器,在多线程环境下,我们如何来保证它们的线程安全性呢?

依靠程序员自己去编写代码,比如对操作加锁,来维护容器的线程安全性,一来耗费开发时间,二来性能没有保证
为了解决这个问题,JUC 针对常用的容器,对应开发了高性能的并发容器,在多线程编程中,我们直接使用这些现成的并发容器即可

从本节开始我们就来详细讲解一下,JUC 并发容器的用法和实现原理
在正式开始之前,本节先对 JUC 并发容器做一个框架性的介绍,让你对 JUC 并发容器有一个系统性的认识

1、Java 容器回顾

在讲解 JUC 并发容器之前,我们先来回顾一下 Java 提供的普通容器,也就是 JCF(Java Collections Framework),在第 11 节中,我们将 Java 容器简单分为以下 5 类

  • List:ArrayList、LinkedList、Vector(废弃)
  • Stack:Stack(废弃)
  • Queue:ArrayDeque、LinkedList、PriorityQueue
  • Set:HashSet、LinkedHashSet、TreeSet
  • Map:HashMap、LinkedHashMap、TreeMap、HashTable(废弃)

在上述 Java 容器中,除了 Vector、Stack、HashTable 之外,绝大部分容器在设计实现时都没有考虑线程安全问题
Vector、Stack、HashTable 这三个容器是 JDK 1.0 引入的,在 JDK 1.2 引入完善的 JCF 之后,这三个容器就被废弃了
只是为了兼容老的 Java 项目代码,Java 并没有将这三个容器直接从 JDK 中移除而已,在平时的项目开发中,我们尽量不要使用这三个类

2、Java 并发容器

为了更符合程序员的开发习惯,JCF 将非线程安全容器和线程安全容器(也就是并发容器)分开来设计实现

  • 在非多线程环境下,我们使用没有加锁的非线程安全容器,性能更高,例如替代 Vector,JCF 设计了非线程安全的 ArrayList
  • 在多线程环境下,我们使用 Collections 工具类提供的并发方法(如 synchronizedList()),将非线程安全容器(如 List)转换为线程安全容器(如 SynchronizedList)来使用

Java 提供的并发容器有以下几种
image

以上容器都是 Collections 类的内部类,我们一般通过 Collections 类提供的以下工具方法来创建并发容器的对象
image

我们拿 SynchronizedList 举例讲解,我们通过 synchronizedList() 方法来创建 SynchronizedList 对象,具体的创建方法如下所示

List list = Collections.synchronizedList(new LinkedList<>());

Java 并发容器的底层实现原理非常简单,跟 Vector、Stack、HashTable 类似,都是通过对方法加锁来避免线程安全问题,我们拿 SynchronizedList 举例讲解,其源码如下所示
所有的函数都使用 synchronized 加了锁,实际上我们还可以使用 ReentrantReadWriteLock 替代 synchronized 来提高代码的并发性能

public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {
return list.equals(o);
}
}
public int hashCode() {
synchronized (mutex) {
return list.hashCode();
}
}
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
public E set(int index, E element) {
synchronized (mutex) {
return list.set(index, element);
}
}
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
// ... 省略其他方法 ...
}

3、JUC 并发容器

从上述 SynchronizedList 的源码,我们可以发现:Java 提供的并发容器的代码实现非常简单,但同时也因为锁粒度大而导致并发性能不高
于是 JUC 便实现了一套更高性能的并发容器,JUC 并发容器之所以比 Java 并发容器性能更高
是因为 JUC 利用:分段加锁、写时复制、无锁编程等技术,对并发容器做了全新的实现,并非像 Java 并发容器那样只是对原有 Java 容器简单加锁
当然从代码实现复杂度上,JUC 并发容器的代码实现要比 Java 并发容器的代码实现复杂得多

JUC 提供的并发容器如下图所示,我们对其进行了简单的分类
image

从上图中我们可以发现,JUC 提供的并发容器并不能跟 Java 容器一一对应,也就是说:有些 Java 容器在 JUC 并没有对应的线程安全版本,比如 LinkedList
相反,Java 提供的并发容器是可以跟 Java 容器一一对应的,任何一个 Java 容器都可以使用 Collections 类提供的 synchronizedXXX() 函数转化为线程安全版本

对于部分 Java 容器,之所以 JUC 没有提供对应的并发容器
是因为对于这些容器,JUC 无法通过非加锁方式(比如无锁编程、写时复制、分段加锁)来提高并发性能,无法提供比 Java 并发容器更高性能的并发容器
对于这部分 Java 容器,在多线程环境下,我们要么使用 Java 提供的并发容器,要么使用替代的 JUC 并发容器

  • 对于 LinkedList,我们只能使用 Java 提供的并发容器 SynchronizedList
  • 对于 TreeSet 和 TreeMap,尽管 JUC 没有提供对应的并发容器,但是我们可以使用 ConcurrentSkipListSet 和 ConcurrentSkipListMap
    它们在功能上完全可以替代 TreeSet 和 TreeMap,既支持快速查找,又支持有序遍历
  • 对于栈这种数据结构,其完全可以被双端队列(Deque)替代,因此不管是在 JDK 1.2 之后的 JCF 中,还是 JUC 中,都没有提供栈相关的容器

4、课后思考题

Java 废弃 Vector、Stack、HashTable 这三个容器的主要原因是什么呢?提示:废弃的主要原因并非是因为它们的并发性能低

JCF 为了统一设计将这三个容器废弃,JCF 将非线程安全容器和线程安全容器(也就是并发容器)分开来设计

  • 在非多线程环境下,我们使用没有加锁的非线程安全容器,性能更高,例如替代 Vector,JCF 设计了非线程安全的 ArrayList
  • 在多线程环境下,我们使用 Collections 工具类提供的并发方法(如 synchronizedList()),将非线程安全容器(如 List)转换为线程安全容器(如 SynchronizedList)
posted @   lidongdongdong~  阅读(37)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开