jdk8集合类汇总介绍

来源:互联网 发布:mac怎么去掉dashboard 编辑:程序博客网 时间:2024/06/05 19:28

JDK1.8数据存储容器实现类介绍

      这篇主要介绍jdk1.8的一些容器实现类(集合+映射(map))的作用和线程安全与否以及实现线程安全的方式,因为jdk提供的集合类挺多的,所以篇幅有些长,大家可以跳常用的几个看如ArrayList、HashMap、ConcurrentHashMap等。

特殊词汇说明:

1)cas操作:Compare and Swap或者Compare and Set,比较并操作,CPU指令,在大多数处理器架构,包括IA32、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

2)DCL: 也就是Double Check Lock,双重检查锁定,解决jmm当初给某一引用赋值时先分配内存然后就赋值该内存地址在执行初始化方法的漏洞,其实jdk1.8后的集合许多实现是多重检查机制,防止在无锁情况下并发问题。

3)happen-before原则: 它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们解决在并发环境下两操作之间是否可能存在冲突的所有问题,它的规则是jvm进行指令重排的依据之一。它的原则如下:

1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

它的规则是(前4):

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

4)Spliterator: Spliterator是一个可分割迭代器(splitable iterator),是jdk1.8后对集合优化的一个亮点,之前的iterator是顺序遍历迭代器,只能单线程的一个个元素的遍历这个集合,而Spliterator就是为了并行遍历元素而设计的一个迭代器,充分利用现代CPU多核的计算能力。


JDK提供的集合类型主要分四种类型:

(1)List  支持null元素和重复元素的动态扩容列表,jdk提供的实现类有:ArrayList, LinkedList, Stack,CopyOnWriteArrayList、Vector

 (2)Set 不支持重复元素的动态扩容列表,jdk提供的实现类有:EnumSet, TreeSet, HashSet, LinkedHashSet、 NavigableSet、ConcurrentSkipListSet、CopyOnWriteArraySet

(3)map  是存储键/值对的映射集,jdk提供的实现类有:HashMap, TreeMap,LinkedHashMap、ConcurrentHashMap、HashTable、ConcurrentSkipListMap

(4)queue/deque  queue是在集合尾部添加元素,在头部删除元素的队列,deque是可在头部和尾部添加或者删除元素的双端队列,

jdk提供的实现类有:ArrayDeque、 PriorityQueue、LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、ArrayBlockingQueue、ConcurrentLinkedDeque、ConcurrentLinkedQueue


接下来介绍下具体这些容器实现类:

List

线程不安全的子类:

ArrayList, LinkedList,

ArrayList类简介:

是一个容量动态扩张的集合,实现了RandomAccess接口,支持随机访问,初始容量10,最大容量Integer.MAX_VALUE - 8(2147483640),每次调用ArrayList的新增或者删除等修改方法,继承自AbstactList抽象类的属性modCount都会自增,当通过Interactor遍历集合时,只要modCount被其他线程修改,就会抛出ConcurrentModificationException。ArrayList是线程不安全的类,因为它的操作自身集合属性的方法没有进行同步也不是原子性操作,所以会出现不一致现象,可以通过List list = Collections.synchronizedList(new ArrayList(...))把它转成线程安全的集合,当然只是封装了对ArrayList的操作,保存同步而已,性能不是很高,所有的修改操作都要一个个同步。

jdk1.8后支持Spliterator迭代器。

LinkedList类简介:

LinkedList内部是链表结果,非线程安全,修改链表结构的操作需要进行同步,如何有线程在用Interator遍历,而有线程在修改链表,会引发fast-fail,及Interator会抛出ConcurrentModificationException,可以使用Collections.synchronizedList方法来包装它。

添加元素的方法只是新增一个节点然后改变尾部节点和新增节点的引用链接,所以新增和删除操作比较快,但是不支持随机访问,判断某个值是否存在的方法contains(Object o)需要从第一个元素开始遍历到符合条件的元素止,效率不是很高。

LinkedList同时实现List和Deque接口,所以即可以当一个双端队列使用,也可以当List使用。

jdk1.8后支持Spliterator迭代器。

线程安全的子类:

CopyOnWriteArrayList、Vector,stack

CopyOnWriteArrayList简介:

CopyOnWriteArrayList实现了List接口,是线程安全的List,主要通过三个关键点来包装线程安全:

1)修改方法(add、set、remove等)都通过集合属性一个ReentrantLock进行同步,先获取锁,才能执行变更操作。但是通过ReentrantLock进行同步只是能保证线程的安全,并不能保持时间上的有序和正确,因为先申请锁然后进入休眠等待的线程,并不一定是最先获取锁的线程,所以,会在时间顺序看,对集合的修改是无序的。

2)对象数组用volatile修饰,其他线程对集合元素数组的修改,能够在其他线程的每次访问都是最新值。volatile如何影响线程栈读取内存变量,下一篇会单独说。

3)在对集合元素数组进行修改时,是先拷贝之前的元素数组出一个新元素数组,在新的元素数组上进行修改,修改完毕后在用元素数组替换旧的元素

数组,这样的内存消耗很大。第一个点已经说了修改集合元素的方法都加了锁,为什么这里获取锁后对集合元素的修改,还要通过拷贝数组的方式?因为拷贝出来的新数据修改完毕后赋予CopyOnwriteArrayList,数据存储的对象数组地址就更改了,已经创建的Interator对对象的数组的引用因为是final修饰的,所以还用的是旧的对象数组地址,所以这样就可以保证已经创建的Interator不受其他线程修改操作的影响。

jdk1.8后支持Spliterator迭代器。

Vector简介:

Vector是实现了List接口和RandomAccess接口的集合类,相对线程安全的,意思是多个线程同时对Vector的同一个修改方法进行操作是安全的,因为Vector对修改数据存储结构的方法如(add、romove)都加了syncronize进行同步。但是如何一个线程创建了Iterator,并进行遍历,那么另一个线程对Vector数据存储的修改都会让Iterator抛出ConcurrentModificationException异常。如果创建Vector实例时不指定每次扩容大小,默认为当下容量的两倍。

jdk1.8后支持Spliterator迭代器。

Stack简介:

stack是一个先进后出的集合,继承自Vecoter类,所以包含不属于栈操作insert和remove,它是线程安全的,因为stack自身的pop、peek、search是用synchronized修饰的同步方法,而Push是自己调用线程安全的vector的addElement方法。Stack是没有In

如果在想使用先进后出这种数据集合的话,建议使用ConcurrentLinkedDeque,在一端插入和删除元素,性能会比Stack好,因为它的同步策略是通过CAS和多重检查机制的无锁策略,比Stack这种在方法前加synchronized进行同步的要高效。


 

set

线程不安全的子类:

EnumSet, TreeSet, HashSet, LinkedHashSet、 NavigableSet

EnumSet简介:

EnumSet是枚举类的容器,是一个抽象类,非线程安全,内部提供静态方法noneOf(Enum.class clazz)来创建一个实现了继承自EnumSet类的实例,如果要装载的枚举类值不超过64个,则创建的是RegularEnumSet实例,如果超过64位,则创建的是JumboEnumSet。

RegularEnumSet内部通过Bit数组来存放枚举值,而这个Bit数组其实就是一个Long类型数值,初试时是0L,添加元素时,是把对应枚举元素的ordinal(每一个枚举类的枚举值都对应一个ordinal值)值映射到64Bit上的某一个位置为1。因为Set是不能重复的,所以RegularEnumSet最多存64个元素,RegularEnumSet的add方法源码如下:

    public boolean add(E e) {
        typeCheck(e);

        long oldElements = elements;
        elements |= (1L << ((Enum<?>)e).ordinal());
        return elements != oldElements;
    }

JumboEnumSet内部则通过long数组类存放枚举值,所以可以存放远远大于64个元素,当然枚举值太多也没有意义。

TreeSet简介:

treeSet是有序集合,内部通过TreeMap来存储元素,把元素存储在map的key里,通过TreeMap存储Key的有序性和无重复性来实现自己的有序性和Set的的元素无重复性;插入元素时,会根据元素的equals和compareTo方法判断大小,然后进行排序,但也只是插入的时候会进行排序,插入后修改顺序不改变。

TreeSet不是线程安全的,如何有多个线程访问TreeSet并至少有一个线程修改它的话,应该进行操作同步或者通过Collections.synchronizedSortedSet方法来创建一个包装TreeSet的线程安全的有序Set,如:SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

jdk1.8后支持Spliterator迭代器。

HashSet简介:

允许null元素,没有实现RandomAccess接口,所以不支持随机访问,需要通过Interator进行遍历。它的内部存储是存储在一个内部属性HashMap的key里,因此它的性能深受两个参数initialCapacity, loadFactor的影响,一个是初始容量,一个是负载因子,这两个参数是作用于hashMap的,因此HashSet的初始大小不要设置太大,不然它的Interator会遍历完HashMap里存储元素的数组,不管是空和还是有元素存着,因为HashMap存储元素是根据hashCode做转换得到具体数组下标位置,会分散在整个存储数组,所以Interator遍历次数等于容量大小加上hash碰撞的次数(buckets数)。

HashSet不是线程安全的,如果有多个线程同时访问,并且至少一个线程在做修改,应该要对线程进行同步或者通过Collections.synchronizedSet方法把它转为线程安全的集合,如:

 Set s = Collections.synchronizedSet(new HashSet(...));

如果有线程创建了HashSet的遍历迭代器Interator,那么其他线程对HashSet的修改会让Interator抛出ConcurrentModificationException。

jdk1.8后的HashSet提供了spliterator()方法来创建Spliterator,通过Spliterator的forEachRemaining()方法可以并行的遍历HashSet的元素,Spliterator后续会单独介绍。

LinkedHashSet简介:

LinkedHashSet是继承自HashSet的一个集合类,没添加或者覆盖什么方法,建议使用HashSet就可以了。

NavigableSet 简介:

NavigableSet是继承自SortSet的接口,主要是弥补Interator遍历器的单一,提供了边界搜索,如:方法lower, floor, ceiling, higher,lower时找出小于目标元素的最大元素;如果没有则返回Null,floor方法是返回小于或者等于目标元素的最大元素,如果不存在的返回Null;ceiling是返回最小的大于或者等于目标元素的元素,不存在则返回null:higher方法则是返回最小的大于目标元素的元素,不存在则返回null。

它还提供一些获取子集视图的方法,如descendingSet、subSet、headSet、tailSet;

descendingSet方法返回该集合中包含的元素的反向顺序视图,对返回的集合的操作会同步到源集合,反过来也一样。

subSet返回指定坐标区间的子集,对返回的集合的操作会同步到源集合,反过来也一样。

headSet返回的是小于或者等于目标元素的子集,对返回的集合的操作会同步到源集合,反过来也一样。

tailSet返回时大于或者等于目标元素的子集,对返回的集合的操作会同步到源集合,反过来也一样。

线程安全的子类:

ConcurrentSkipListSet、CopyOnWriteArraySet

ConcurrentSkipListSet简介:

ConcurrentSkipListSet是实现了NavigableSet接口和继承自AbstractSet的Set集合类,它是线程安全的,内部存储实际是存在ConcurrentNavigableMap中。内部元素的存储是有序的,根据创建ConcurrentSkipListSet时提供的Comparator。

对它的操作如 contains, add, remove 平均时间是logthumb_down,升序视图及其迭代器比降序视图要快。

它的容量变更的操作方法如:addAll、removeAll、retainAll、containsAll、containsAll、toArray并不能保证原子性,如在使用Interator遍历集合时,另一个线程进行addAll操作,那么iterator操作可能只能看到部分新添加的元素。

jdk1.8后支持Spliterator迭代器。

CopyOnWriteArraySet简介:

CopyOnWriteArraySet底层实现是CopyOnWriteArrayList,线程安全的,大部分操作和原理是同CopyOnWriteArrayList,包括创建遍历迭代器用的也是CopyOnWrietArrayList的,每次创建的迭代器获取的数据都是一个快照,不需要进行同步,其他线程对数据的修改不影响已经创建的Interator,它最适合于具有以下特征的应用程序:set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。  

因为通常需要复制整个基础数组,所以可变操作(add、set 和 remove 等等)的开销很大。

jdk1.8后支持Spliterator迭代器。


map

线程不安全的子类:

HashMap, TreeMap,LinkedHashMap

HashMap简介:

HashMap是线程不安全的提供所有Map操作的集合容器,支持Null为key或者value,并且是无序的。因为HashMap不是线程安全的,所以如果有个线程访问该级赫尔,并且至少一个线程在修改的话,应该进行修改操作的同步,或者使用Collections.synchronizedMap把它转成线程的安全的Map。

需要特别介绍下jdk1.8后HashMap的存储结构,因为和1.7之前都不太一样,性能也不一样。

jdk1.8之前,hashMap的存储结构是数组+链表,也就是发生hash冲突后的元素后插入到链表里,而不是存储在数组,这样的的话如果对于hash冲突比较严重的数据时,hashMap的查询速度就不是o(1)了,而是o('n');

所以,jdk1.8后,HashMap的存储结构是数组+链表or平衡树,因为平衡树的时间复杂度是o(log('n')),是1.8后HashMap的查询复杂度是O(1)~O(log('n' ))。

初始化HashMap最重要的也是最影响性能的参数是:loadFactor、initialCapacity、TREEIFY_THRESHOLD(不可变)、UNTREEIFY_THRESHOLD(不可变)。

loadFactor是触发HashMap扩容的负载因子,默认是0.75,扩容是非常耗时的事情,所以这个负载因子很重要,0.75是性能比较好的一个数;initialCapacity是初始HashMap容量,因为HashMap迭代器Interator遍历时是会遍历整个存储数组和链表或者平衡树,所以initialCapacity也不应设太大。TREEIFY_THRESHOLD是触发链表转化成平衡树的阈值(同一个数组位置发生哈希碰撞而产生链表的数量),默认是8;UNTREEIFY_THRESHOLD是扩容后,原先发送哈希冲突的地方会减少,所以该值是发送扩容后平衡树转链表的哈希冲突数量阈值,默认是6。

TreeMap简介:

TreeMap是一个基于红黑树存储结构的实现了NavigableMap和继承自AbstractMap的有序Map集合。它存储元素的排序依赖于键的自然排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 logthumb_down 。它不是线程安全的,如果多个线程同时访问该集合,并且至少一个线程在做修改操作的话, 应该要进行操作的同步处理,或者通过Collections.synchronizedSortedMap把它转为线程安全的Map,如:

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

TreeMap的interator方法返回创建遍历集合的支持fail-fast行为的Interator,但是如果在创建了Interator后,有其他线程修改了集合,那么Interator可能会抛出ConcurrentModificationException。

需要特别注意的是,不仅Put方法会改变集合的值,Map.Entry的setValue也是可以修改集合的值,如:

Set<Entry<String, Integer>> set = map.entrySet();
        for(Entry<String, Integer> entry:set){
            entry.setValue(3333);
        }

 

jdk1.8后支持KeySpliterator。

LinkedHashMap简介:

LinkedHashMap是继承自HashMap的有序集合,根据初始化参数accessOrder是false还是true来选择是按插入元素时的循序还是按访问循序进行排序。LinkedHashMap存储结构和HashMap是一样的,最大的不同是,存储节点Entry新增了双向连接,用于指向前一个节点和后一个节点,所以LinkedHashMap同时维持着一个双向链表,遍历元素时可以按链接循序遍历整个集合。其他特性和HashMap差不多。

线程安全的Map:

ConcurrentHashMap、HashTable、ConcurrentSkipListMap

ConcurrentHashMap简介:

ConcurrentHashMap是线程安全的HashMap,实现了ConcurrentMap接口,所以提供了一些原子性和线程安全的集合操作接口,如:

putIfAbsent(K, V)、replace(K, V, V)、compute(K, BiFunction<? super K, ? super V, ? extends V>)等。

JDK1.8之后的ConcurrentHashMap的实现和1.7之前已经大不一样了,当然存储结构还是和HashMap一样,数组+链表or红黑数,但是保证线程安全的机制从原来的给每个数组segment加锁方式变成了无锁的cas操作,特别是扩容方式被重写了,实现了无锁情况下多线程参与复制旧存储元素到新存储集合上。有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。

我们拿最常用的put的方法来说明,如何实现无锁操作:

根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。ConcurrentHashMap中依然沿用这个思想,有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况

  1. 如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点,如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;

  2. 如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

整体流程就是首先定义不允许key或value为null的情况放入  对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。

如果这个位置是空的,那么直接放入,而且不需要加锁操作。

    如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。  如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

因为为了要实现无锁,ConcurrentHashMap新增了许多私有方法和属性,推荐一篇写得很详细的博客:

http://blog.csdn.net/u010723709/article/details/48007881

jdk1.8后支持等流式处理操作包括forEach、map和reduce操作。

HashTable简介:

HashTable是线程安全的HashMap,其实就在修改方法上添加synchronized关键字进行同步,同步的粒度太大,性能不佳,不建议使用。

ConcurrentSkipListMap简介:

ConcurrentSkipListMap是基于SkipList存储结构实现的线程安全的有序Map集合,不支持Null为value或者为key,我们都知道,线程安全的集Map可以用ConcurrentHashMap,有序的Map可以用TreeMap,但是线程安全且有序的目前ConcurrentSkipListMap。

ConcurrentSkipListMap实现了ConcurrentNavigableMap接口,所以提供了一些导航方法,如查询小于目标key的最大entry、大于目标Key的最小entry、最开始的entry,最末,的查询的最好时间复杂度平均为O(log n),最差是Othumb_down,主要是用空间换时间,节点上冗余了其他节点的引用并且低曾包含上一层所有节点信息,来实现快速查找,可以看下面的SkipList结构介绍。

SkipList是类似于LinedList的链表,但是不同的是LinkedList存储的是前后节点的引用,访问的时候是依次访问,而SkipList是存的是跨越多个点的引用,如下图:低一层包含比上一层更多的节点,上一层的节点跨度比较大,SkipList里层信息有Index维护,Index包含下一级index的引用,同一层数据节点的头节点的引用,数据存储由Node节点维护,每个Node节点维护右边比自己Key大的节点的引用,以及下一层自己的引用。

在查询的时候,比如下图查询key为45的节点,先从Inded3查询第一层,发现45大于3小于62,于是查询到下一层,发现45大于3、大于23小于62,于是从23节点往下走,发现23下一级就是45,查询结束。总共走了5步,这个是效果比较差的情况,和链表查询次数一样。

如果查询的节点是62,那么只需要走两步就找到结果,如果是链表的话需要7步,所以比链表快上不少。每个节点存储多少个节点的引用是随机产生的,而这也是影响SkipList查询效率的主要因素之一。

SkipList层数合适时自顶向下搜索,理想情况下每下降一层,搜索范围减小一半,达到类似二分查找的效果,效率为O(lgn);最坏情况下也只是curr从head移动到tail,效率为Othumb_down

1354281109_6965.png

 

现在来说说ConcurrentSkipListMap如何在无锁情况下实现并发:

核心思想就是通过cas操作来代替锁,如果要执行的操作与预期不一样,就重新执行刚才的动作,直到成功为止。如下doPut部分代码所示

for (;;) {        // 找到key的前继节点        Node<K,V> b = findPredecessor(key);        // 设置n为“key的前继节点的后继节点”,即n应该是“插入节点”的“后继节点”        Node<K,V> n = b.next;        for (;;) {            if (n != null) {                Node<K,V> f = n.next;                // 如果两次获得的b.next不是相同的Node,就跳转到”外层for循环“,重新获得b和n后再遍历。                if (n != b.next)                    break;

            。。。。

jdk1.8后支持KeySpliterator。


 

queue

线程不安全的子类

ArrayDeque、 PriorityQueue

ArrayDeque简介:

ArrayDeque是一个实现了Deque接口的双端队列,是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行。不支持Null元素,不是线程安全的,把它当栈时,也就是只允许从一端插入数据和获取,比Stack快;把它当队列时它即从一端插入,从另一端获取数据,比LinkedList要快。大多数操作都是在平均的常数时间内运行的,除了如remove 、removeFirstOccurrence 、  removeLastOccurrence、contains、 iterator.remove()这些是在线性的时间内完成即数数据增加而线性增加。

ArrayDeque的iterator是支持fast-fail的,也就是如果创建了遍历迭代器Interator,那么在此期间其他线程修改了ArrayDeque,那么Interator很大可能会抛出ConcurrentModificationException。

ArrayDeque是大小自增长的队列,内部使用数组存储数据、内部数组长度每次扩容都是翻倍的增长,为8、16、32….. 2的n次方,头指针head从内部数组的末尾开始,尾指针tail从0开始,在头部插入数据时,head减一,在尾部插入数据时,tail加一。当head==tail时说明数组的容量满足不了当前的情况,此时需要扩大容量为原来的二倍。

另外,从jdk1.8后,新增了Spliterator迭代器,支持并行遍历数据。

PriorityQueue简介:

PriorityQueue是实现了AbstractQueue接口的优先队列,它不是线程安全的,也就是每次取出的元素都是队列中key最小的,元素大小的评判可以通过元素本身的自然顺序(natural ordering),也可以通过构造时传入的比较器Comparator。不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),PriorityQueue的底层实现是通过数组来构建小项堆。如下图所示:

priorityQueue.PNG

jdk.18后支持Spliterator。

线程安全的子类

LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、ArrayBlockingQueue、ConcurrentLinkedDeque、ConcurrentLinkedQueue

LinedBlockingDeque简介:

LinkedBlockingDeque是双向链表实现的双向并发阻塞队列,线程安全,该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。

此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

它的线程安全主要通过每个操作方法的内需要通过重入锁ReentrantLock进行同步,如下图代码,所有的方法需要获取这个主锁才能执行,从而保证了只有一个线程能进入修改LinkedBlockingDeque的方法,同时有两个条件Condition,一个是notEmpty,一个是notFull,当有元素新增进LinkedBlockingDeque时,notEmpty激活在notEmpty上等待线程,当新增元素时队列满了,需要在notFull上等待直到有线程消费元素后激活notFull。

新增元素的代码如下:

  public void putLast(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
try {
            while (!linkLast(node))
  notFull.await();
        } finally {
            lock.unlock();
        }
    }

 

LinkedBlockingQueue简介:

LinkedBlockingQueue是一个单向链表实现的阻塞队列,先进先出的顺序。支持多线程并发操作,线程安全,它是无界的队列,LinkedBlockingQueue继承AbstractQueue,实现了BlockingQueue,Serializable接口。内部使用单向链表存储数据,默认初始化容量是Integer最大值,消费时从头部获取元素,新增时从尾部新增。

插入和取出使用不同的锁,putLock插入锁,takeLock取出锁,添加和删除数据的时候可以并行。多CPU情况下可以同一时刻既消费又生产。

jdk1.8后也支持Spliterator。

PriorityBlockingQueue简介:

PriorityBlockingQueue是线程安全PriorityQueue,和PriorityQueue相比,主要是新增和消费元素方法需要获取一个ReentrantLock锁进行同步,从而保证了线程的安全,peek()是当队列无元素时,返回空,take()是当队列无元素时一直notEmpty这个条件上等待,直到有线程往队列新增元素时,才会从新把notEmpty条件激活,take()才从新往下执行。

特别注意的是PriorityBlockingQueue的序列化输出方法其实是把自己转成PriorityQueue然后在调PriorityQueue序列化输出方法。

1.8后支持Spliterator并行流式迭代器。

ArrayBlockingQueue简介:

ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,先进先出(FIFO),初始化ArrayBlockingQueue时必须指定初始容量,并且初始化后容量大小不可变。它的线程安全的实现机制和PriorityBlockingQueue一样,每个操作方法需要先获取同一个ReentrantLock进行同步。

1.8后支持Spliterator并行流式迭代器。

ConcurrentLinkedDeque简介:

它是一个基于链表的无界的线程安全的非堵塞双端队列,不支持Null元素,对于新增、删除、查询都是线程安全的。但是它的size方法不是常数时间的,它需要每次遍历统计才能知道数据量的大小,并且有可能在遍历统计过程中有其他线程修改了元素导致统计不准确。

 像 addAll、removeAll, retainAll, containsAll,equals,  toArray 操作都不能保证原子性,有可能获取到的结果是中间结果。

它是非堵塞双端队列,也就是peek()获取第一个元素,如果队列为空,则返回null,而不是等待。

线程安全保证是通过CAS操作级多重检查机制(即每次设值前判断当前状态不是未被其他线程修改),比如在头部新增一个对象时,会进入一个无线循环体,然后读取头部节点,如果头节点的下一个节点是null,则表示这是一个空集合,然后设置用cas操作新节点newNode的next节点为head节点p,p节点设置通过cas设置pre节点为新节点newNode并且判断设置之前pre节点是Null,p节点pre不是Null则表示有其他线程在修改,则退回外循环从新获取头节点进行判断。如果设置成功,则判断p节点是不是头节点,如果不是,表示p是数据节点,则需要把newNode通过cas设置为头节点!具体代码如下:

private void linkFirst(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);

        restartFromHead:
for (;;)
    for (Node<E> h head, p h, q;;) {
        if ((q = p.prev) != null &&
                    (q = (p = q).prev) != null)
            //没一次循环都要检查头部更新
            // 如果h不等于head节点,则p =q,
                    p = (h != (h = head)) ? h : q;
        else if (p.next == p) //如果p的next节点等于自己,那么跳出循环,从新进入循环体
            continue restartFromHead;
                else {
            // p 是头节点
                    newNode.lazySetNext(p); // CAS 设置新节点NewNode的next节点是p
                    if (p.casPrev(null, newNode)) {
                if (p != h) // hop two nodes at a time
                            casHead(h, newNode);  //如果设置失败也是okay
                return//设置成功返回
                    }
            
                }
            }
    }

jdk1.8后也提供Spliterator迭代器。

ConcurrentLinkedQueue简介:

它是一个线程安全的无界先进先出(FIFO)的队列,只能从头部取数据,尾部添加数据,并且不允许存储Null元素,它的迭代器Interator创建后,其他线程对队列的修改并不会抛出ConcurrentModificationException,并且还能弱一致的表示当前队列的状态,其他线程对队列的修改它也可以获取,这是因为它创建迭代器的和遍历元素用的是同一个方法,只是创建Interator时不返回元素,Next()方法返回元素,也就是每遍历下一个元素时就相当于重新创建一个Interator,当时如果其他线程对队列的修改发生在一次迭代中间,那么本次迭代Next返回的极少可能是不准确的,但也仅次迭代而已,下一次会又准确了。

它的size方法需要遍历全部元素统计,所以比较费时间并且还不一定准确!

它的容量变更的操作方法如:addAll、removeAll、retainAll、containsAll、containsAll、toArray并不能保证原子性,如在使用Interator遍历集合时,另一个线程进行addAll操作,那么iterator操作可能只能看到部分新添加的元素。

它的线程安全策略和ConcurrentLinkedDeque一样,CAS操作和多重检查

jdk1.8后也支持Spliterator。

原创粉丝点击