Java容器的常见问题

记录Java容器中的常见概念和原理

参考:

基础容器

ArrayList(动态数组)、LinkedList(带头结点的双向链表)

ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 默认初始容量为 10;
  • 扩容机制:添加元素前,先检查是否需要扩容,一般扩为源数组的 1.5 倍 + 1;
  • 边界检查(即检查 ArrayList 的 Size):涉及到 index 的操作;
  • 调整数组容量(减少容量):将底层数组的容量调整为当前列表保存的实际元素的大小;
  • 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理;

LinkedList

LinkedList 不但实现了List接口,还实现了Dequeue接口。因此,LinkedList不但可以当做List来用,还可以当做Stack(push、pop、peek),Queue(offer、poll、peek)来使用。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
   }
}

ArrayList与LinkedList比较

  • ArrayList是基于数组的实现,LinkedList是基于带头结点的双向循环链表的实现;
  • ArrayList支持随机访问,LinkedList不支持;
  • LinkedList可作队列和栈使用,实现了Dequeue接口,而ArrayList没有;
  • ArrayList 寻址效率较高,插入/删除效率较低;LinkedList插入/删除效率较高,寻址效率较低

Fast-fail机制

Fail-Fast 是 Java 集合的一种错误检测机制,为了防止在某个线程在对Collection进行迭代时,其他线程对该Collection进行结构上的修改。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险,抛出 ConcurrentModificationException 异常。

Set (HashSet,LinkedHashSet,TreeSet)

Set不包含重复的元素,这是Set最大的特点,也是使用Set最主要的原因。常用到的Set实现有 HashSet,LinkedHashSet 和 TreeSet。一般地,如果需要一个访问快速的Set,你应该使用HashSet;当你需要一个排序的Set,你应该使用TreeSet;当你需要记录下插入时的顺序时,你应该使用LinedHashSet。

  • HashSet:委托给HashMap进行实现,实现了Set接口
    HashSet是采用hash表来实现的,其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    static final long serialVersionUID = -5024744406713321676L;

    // 底层支持,HashMap可以代表一个HashMap,也可以代表一个LinkedHashMap
    private transient HashMap<E,Object> map;     

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();    // 傀儡对象
  • LinkedHashSet:是HashSet的子类,被委托给HashMap的子类LinkedHashMap进行实现,实现了Set接口
    LinkedHashSet继承于HashSet,利用下面的HashSet构造函数即可,注意到,其为包访问权限,专门供LinkedHashSet的构造函数调用。LinkedHashSet性能介于HashSet和TreeSet之间,是HashSet的子类,也是一个hash表,但是同时维护了一个双链表来记录插入的顺序,基本方法的复杂度为O(1)。
  • TreeSet:委托给TreeMap(TreeMap实现了NavigableSet接口)进行实现,实现了NavigableSet接口(扩展的 SortedSet)
    TreeSet是采用树结构实现(红黑树算法),元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法,它还提供了一些方法来处理排序的set,如first()、 last()、 headSet()和 tailSet()等。此外,TreeSet不同于HashSet和LinkedHashSet,其所存储的元素必须是可排序的(元素实现Comparable接口或者传入Comparator),并且不能存放null值。
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    /**
     * The backing map.
     */
    private transient NavigableMap<E,Object> m;   // 底层支持

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    // 默认构造函数
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

Map(HashMap, LinkedHashMap, TreeMap)

HashMap

  • 对NULL键的特别处理
    • HashMap 中可以保存键为NULL的键值对,且该键值对是唯一的。若再次向其中添加键为NULL的键值对,将覆盖其原值。此外,如果HashMap中存在键为NULL的键值对,那么一定在第一个桶中。
  • HashMap 中的哈希策略
    • 使用 hash() 方法用于对Key的hashCode进行重新计算,以防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使Key的hash值的分布尽量均匀;
    • 使用 indexFor() 方法进行取余运算,以使Entry对象的插入位置尽量分布均匀
  • HashMap 的底层数组长度为何总是2的n次方?
    • 构造函数及扩容策略
// HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n 
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;

*当底层数组的length为2的n次方时,h&(length - 1) 就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化

  • 初始容量16,两倍扩容,先添加再检查

LinkedHashMap:HashMap + 带头结点的双向循环链表


TreeMap:红黑树的实现

  • 排序二叉树 Vs. 红黑树
    排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很差。
  • 红黑树
    为了改变排序二叉树存在的不足,Rudolf Bayer 与 1972 年发明了另一种改进后的排序二叉树:红黑树,他将这种排序二叉树称为“对称二叉 B 树”,而红黑树这个名字则由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年首次提出。
    红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型地,JDK 提供的集合类 TreeMap 本身就是一个红黑树的实现。红黑树在原有的排序二叉树增加了如下几个要求:
    • 性质 1:每个节点要么是红色,要么是黑色;
    • 性质 2:根节点永远是黑色的;
    • 性质 3:所有的叶节点都是空节点(即 null),并且是黑色的;
    • 性质 4:每个红色节点的两个子节点都是黑色(从每个叶子到根的路径上不会有两个连续的红色节点);
    • 性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
  • 红黑树的性质
    上面的性质3中指出:红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。根据性质 5,红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度(black-height)”。性质 4 则保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的两倍。
    假如有一棵黑色高度为 3 的红黑树:从根节点到叶节点的最短路径长度是 2,该路径上全是黑色节点(黑节点 - 黑节点 - 黑节点)。最长路径也只可能为 4,在每个黑色节点之间插入一个红色节点(黑节点 - 红节点 - 黑节点 - 红节点 - 黑节点),性质 4 保证绝不可能插入更多的红色节点。由此可见,红黑树中最长路径就是一条红黑交替的路径。由此我们可以得出结论:对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 * (N-1)。
  • 红黑树的查找、插入与删除操作
    由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。
  • 红黑树和平衡二叉树
    我们知道,在AVL树中,任何节点的两个子树的最大高度差为1,所以它也被称为高度平衡树;而红黑树并不追求 完全平衡,它只要求大致地达到平衡要求,降低了对旋转的要求,从而提高了性能。实际上,由于红黑树的设计,任何不平衡都会在三次旋转之内解决。
    红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
    红黑树并不是真正的平衡二叉树,但在实际应用中,红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
    排序二叉树的深度直接影响了检索的性能,正如前面指出,当插入节点本身就是由小到大排列时,排序二叉树将变成一个链表,这种排序二叉树的检索性能最低:N 个节点的二叉树深度就是 N-1。
    红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。

容器中判断两个对象是否相等的步骤

  • 判断两个对象的hashCode是否相等:如果不相等,认为两个对象也不相等,完毕;如果相等,转入2);
  • 判断两个对象用equals运算是否相等:如果不相等,认为两个对象也不相等;如果相等,认为两个对象相等。

同步容器

主要参考:https://github.com/wangzhiwubigdata/God-Of-BigData#三Java并发容器

  • List和Set
    JUC(java.util.concurrent)集合包中的List和Set实现类包括: CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentSkipListSet
    • CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。
    • CopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。
  • Map
    JUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。
    • ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。
    • ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。
    • ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。
      JUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。
  • Queue
    JUC集合包中Queue的实现类包括: ArrayBlockingQueue, LinkedBlockingQueue, LinkedBlockingDeque, ConcurrentLinkedQueue和ConcurrentLinkedDeque。
    • ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。
    • LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按 FIFO(先进先出)排序元素。
    • LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式。
    • ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按 FIFO(先进先出)排序元素。
    • ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

CopyOnWrite

CopyOnWrite 容器即写时复制的容器,从JDK1.5开始,Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器 —— CopyOnWriteArrayList 和 CopyOnWriteArraySet,它们适用于 读操作远多于写操作 的并发场景中。关于写时复制容器,通俗的理解是,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是,我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以,CopyOnWrite容器也是一种 读写分离思想,读和写不同的容器。写时复制容器也存在一些缺点,比如:

  • 容器对象的复制需要一定的开销,如果对象占用内存过大,可能造成频繁的YoungGC和Full GC,对内存的消耗太大;
  • CopyOnWriteArrayList不能保证数据严格的实时一致性,只能保证最终一致性。

LinkedBlockingQueue

LinkedBlockingQueue是一个单向链表实现的阻塞队列。该队列按 FIFO(先进先出)排序元素,新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
此外,LinkedBlockingQueue还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

  • LinkedBlockingQueue继承于AbstractQueue,它本质上是一个FIFO(先进先出)的队列。
  • LinkedBlockingQueue实现了BlockingQueue接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
  • LinkedBlockingQueue是通过单链表实现的: (01) head是链表的表头。取出数据时,都是从表头head处插入。 (02) last是链表的表尾。新增数据时,都是从表尾last处插入。(03) count是链表的实际大小,即当前链表中包含的节点个数。(04) capacity是列表的容量,它是在创建链表时指定的。 (05) putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制。 LinkedBlockingQueue在实现“多线程对竞争资源的互斥访问”时,对于“插入”和“取出(删除)”操作分别使用了不同的锁。对于插入操作,通过“插入锁putLock”进行同步;对于取出操作,通过“取出锁takeLock”进行同步。 此外,插入锁putLock和“非满条件notFull”相关联,取出锁takeLock和“非空条件notEmpty”相关联。通过notFull和notEmpty更细腻的控制锁。

LinkedBlockingDeque

LinkedBlockingDeque是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。
此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

  • LinkedBlockingDeque继承于AbstractQueue,它本质上是一个支持FIFO和FILO的双向的队列。
  • LinkedBlockingDeque实现了BlockingDeque接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
  • LinkedBlockingDeque是通过双向链表实现的。
    • first是双向链表的表头, last是双向链表的表尾,count是LinkedBlockingDeque的实际大小,即双向链表中当前节点个数,capacity是LinkedBlockingDeque的容量,它是在创建LinkedBlockingDeque时指定的
    • lock是控制对LinkedBlockingDeque的互斥锁,当多个线程竞争同时访问LinkedBlockingDeque时,某线程获取到了互斥锁lock,其它线程则需要阻塞等待,直到该线程释放lock,其它线程才有机会获取lock从而获取cpu执行权。
    • notEmpty和notFull分别是“非空条件”和“未满条件”。通过它们能够更加细腻进行并发控制。

CopyOnWriteArrayList

它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:

  • 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  • 它是线程安全的。
  • 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  • 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  • 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
    数据结构说明:
  • CopyOnWriteArrayList实现了List接口,因此它是一个队列。
  • CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
  • CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
    下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList的原理进行说明。
  • CopyOnWriteArrayList的“动态数组”机制 -- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。
  • CopyOnWriteArrayList的“线程安全”机制 -- 是通过volatile和互斥锁来实现的。
    • CopyOnWriteArrayList是通过"volatile数组"来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了"读取到的数据总是最新的"这个机制的保证。
    • CopyOnWriteArrayList通过互斥锁来保护数据。在"添加/修改/删除"数据时,会先"获取互斥锁",再修改完毕之后,先将数据更新到“volatile数组”中,然后再"释放互斥锁",这样,就达到了保护数据的目的。

CopyOnWriteArraySet

它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。 和CopyOnWriteArrayList类似,CopyOnWriteArraySet具有以下特性:

  • 它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  • 它是线程安全的。
  • 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  • 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作。
  • 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
    数据结构说明:
  • CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合。
  • CopyOnWriteArraySet包含CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。而CopyOnWriteArrayList本质是个动态数组队列,所以* CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”! CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!
  • CopyOnWriteArraySet的"线程安全"机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的。

ConcurrentSkipListMap

ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。 ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。 第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。 关于跳表(Skip List),它是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。

数据结构说明:

  • 跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。每一层的数据都是有序的,上一层数据是下一层数据的子集,并且第一层(level 1)包含了全部的数据;层次越高,跳跃性越大,包含的数据越少。 跳表包含一个表头,它查找数据时,是从上往下,从左往右进行查找。
  • ConcurrentSkipListMap继承于AbstractMap类,也就意味着它是一个哈希表。
  • Index是ConcurrentSkipListMap的内部类,它与“跳表中的索引相对应”。HeadIndex继承于Index,ConcurrentSkipListMap中含有一个HeadIndex的对象head,head是“跳表的表头”。
  • Index是跳表中的索引,它包含“右索引的指针(right)”,“下索引的指针(down)”和“哈希表节点node”。node是Node的对象,Node也是ConcurrentSkipListMap中的内部类。

ConcurrentSkipListSet

ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。 ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是, 第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。 第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。
数据结构说明:

  • ConcurrentSkipListSet继承于AbstractSet。因此,它本质上是一个集合。
  • ConcurrentSkipListSet实现了NavigableSet接口。因此,ConcurrentSkipListSet是一个有序的集合。
  • ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的。它包含一个ConcurrentNavigableMap对象m,而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap的实例。ConcurrentSkipListMap中的元素是key-value键值对;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key!

ConcurrentLinkedQueue

ConcurrentLinkedQueue是线程安全的队列,它适用于“高并发”的场景。 它是一个基于链接节点的无界线程安全队列,按照 FIFO(先进先出)原则对元素进行排序。队列元素中不可以放置null元素(内部实现的特殊节点除外)。
数据结构说明:

  • ConcurrentLinkedQueue继承于AbstractQueue。
  • ConcurrentLinkedQueue内部是通过链表来实现的。它同时包含链表的头节点head和尾节点tail。ConcurrentLinkedQueue按照FIFO(先进先出)原则对元素进* * 行排序。元素都是从尾部插入到链表,从头部开始返回。
  • ConcurrentLinkedQueue的链表Node中的next的类型是volatile,而且链表数据item的类型也是volatile。关于volatile,我们知道它的语义包含:"即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入"。ConcurrentLinkedQueue就是通过volatile来实现多线程对竞争资源的互斥访问的.

ConcurrentHashMap

HashMap是我们平时开发过程中用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。解决方案有Hashtable和Collections.synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能可想而知。

JDK1.7的实现

ConcurrentHashMap采用 分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构。 其包含两个核心静态内部类 Segment和HashEntry。
Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶。 HashEntry 用来封装映射表的键/值对; 每个桶是由若干个 HashEntry 对象链接起来的链表。 一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,下面我们通过一个图来演示一下 ConcurrentHashMap 的结构:

JDK 1.8的实现

1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

总结

ConcurrentHashMap是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于HashTable和同步包装器包装的 HashMap,使用一个全局的锁来同步不同线程间的并发访问,同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器,这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。

ConcurrentHashMap,HashMap 与 HashTable的区别

  • 本质:三者都实现了Map接口,ConcurrentHashMap和HashMap是AbstractMap的子类,HashTable是Dictionary的子类;
  • 线程安全性:HashMap 是线程不安全的,但 ConcurrentHashMap 和 HashTable 是线程安全的,但二者保证线程安全的策略不同;前者采用的是分段锁机制,默* 认理想情况下,可支持16个线程的并发写和任意线程的并发读,效率较高;HashTable 采用的是同步操作,效率较低
  • 键值约束:HashMap 允许键、值为 null,但 ConcurrentHashMap 和 HashTable 既不允许键为null,也不允许值为 null;
  • 哈希策略:三者哈希策略不同,HashTable是key.hashCode取余;ConcurrentHashMap与HashMap都是先对hashCode进行再哈希,然后再与(桶数 - 1)进行取余运算,但是二者的再哈希算法不同;
  • 扩容机制:扩容检查机制不同,ConcurrentHashMap 和 HashTable 在插入元素前检查,HashMap 在元素插入后检查;
  • 初始容量:HashTable 初始容量 11,扩容 2倍 + 1;HashMap初始容量16,扩容2倍

ArrayBlockingQueue

ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。 线程安全是指,ArrayBlockingQueue内部通过“互斥锁”保护竞争资源,实现了多线程对竞争资源的互斥访问。而有界,则是指ArrayBlockingQueue对应的数组是有界限的。 阻塞队列,是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程需要阻塞等待;而且,ArrayBlockingQueue是按 FIFO(先进先出)原则对元素进行排序,元素都是从尾部插入到队列,从头部开始返回。
注意:ArrayBlockingQueue不同于ConcurrentLinkedQueue,ArrayBlockingQueue是数组实现的,并且是有界限的;而ConcurrentLinkedQueue是链表实现的,是无界限的.
数据结构说明:

  • ArrayBlockingQueue继承于AbstractQueue,并且它实现了BlockingQueue接口。
  • ArrayBlockingQueue内部是通过Object[]数组保存数据的,也就是说ArrayBlockingQueue本质上是通过数组实现的。ArrayBlockingQueue的大小,即数组的容量是创建ArrayBlockingQueue时指定的。
  • ArrayBlockingQueue与ReentrantLock是组合关系,ArrayBlockingQueue中包含一个ReentrantLock对象(lock)。ReentrantLock是可重入的互斥锁,ArrayBlockingQueue就是根据该互斥锁实现“多线程对竞争资源的互斥访问”。而且,ReentrantLock分为公平锁和非公平锁,关于具体使用公平锁还是非公平锁,在创建ArrayBlockingQueue时可以指定;而且,ArrayBlockingQueue默认会使用非公平锁。
  • ArrayBlockingQueue与Condition是组合关系,ArrayBlockingQueue中包含两个Condition对象(notEmpty和notFull)。而且,Condition又依赖于ArrayBlockingQueue而存在,通过Condition可以实现对ArrayBlockingQueue的更精确的访问
    • 若某线程(线程A)要取数据时,数组正好为空,则该线程会执行notEmpty.await()进行等待;当其它某个线程(线程B)向数组中插入了数据之后,会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。此时,线程A会被唤醒从而得以继续运行。
    • 若某线程(线程H)要插入数据时,数组已满,则该线程会它执行notFull.await()进行等待;当其它某个线程(线程I)取出数据之后,会调用notFull.signal()唤醒“notFull上的等待线程”。此时,线程H就会被唤醒从而得以继续运行。
posted @ 2020-01-04 17:03  冰糖ryj  阅读(398)  评论(0编辑  收藏  举报