Java容器相关(2)-- Map、Set、Queue

二、Map

 1)HashMap:

HashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素;除该类未实现同步外,其余跟Hashtable大致相同;跟TreeMap不同,该容器不保证元素顺序,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。 根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。 HashMap采用的是冲突链表方式。

HashMap可以看做是数组和链表结合组成的复合结构,数组被分为一个个桶,通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储。

HashMap的设计与实现主要围绕下面几个方面:

* HashMap内部实现基本点分析。

* 容量(capacity)和负载系数(load factor)

* 树化


拉链法的工作原理:

HashMap map = new HashMap<>();

map.put("K1", "V1");

map.put("K2", "V2");

map.put("K3", "V3");


新建一个 HashMap,默认大小为 16;

插入 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。

插入 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。

插入 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的 不是插在 后面,而是插入在链表头部。使用头插法是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。


查找需要分成两步进行:

计算键值对所在的桶;

在链表上顺序查找,时间复杂度显然和链表的长度成正比。

(1)get()方法的原理

使用Get方法根据Key来查找Value的时候,发生了什么呢?首先通过hash()函数得到对应bucket的下标index,然后依次遍历冲突链表,通过key.equals(k)方法来判断是否是要找的那个entry。

   上图中hash(k)&(table.length-1)等价于hash(k)%table.length,原因是HashMap要求table.length必须是2的指数,因此table.length-1就是二进制低位全是1,跟hash(k)相与会将哈希值的高位全抹掉,剩下的就是余数了。


(2)put()方法的原理

     put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry,插入方式为头插法。因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。使用头插法是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

publicVput(Kkey,Vvalue) {

    if (table == EMPTY_TABLE) {

        inflateTable(threshold);

    }

    // 键为 null 单独处理

    if (key == null)

        returnputForNullKey(value);

    int hash =hash(key);

    // 确定桶下标

    int i = indexFor(hash, table.length);

    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value

    for (Entry e = table[i]; e != null; e = e.next) {

        Objectk;

        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

            VoldValue= e.value;

            e.value =value;

            e.recordAccess(this);

            returnoldValue;

        }

    }


    modCount++;

    // 插入新键值对

    addEntry(hash, key, value, i);

    return null;

}

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap使用第 0 个桶存放键为null 的键值对。

privateVputForNullKey(Vvalue) {

    for (Entry e = table[0]; e != null; e = e.next) {

        if (e.key == null) {

            VoldValue= e.value;

            e.value =value;

            e.recordAccess(this);

            returnoldValue;

        }

    }

    modCount++;

    addEntry(0, null, value, 0);

    return null;

}

使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。

void addEntry(inthash,Kkey,Vvalue,intbucketIndex) {

    if ((size >= threshold) && (null !=table[bucketIndex])) {

        resize(2 * table.length);

        hash= (null != key) ? hash(key) : 0;

        bucketIndex= indexFor(hash, table.length);

    }


    createEntry(hash, key, value, bucketIndex);

}


void createEntry(inthash,Kkey,Vvalue,intbucketIndex) {

    Entry e =table[bucketIndex];

    // 头插法,链表头部指向新的键值对

    table[bucketIndex]= new Entry<>(hash, key, value, e);

    size++;

}

Entry(inth,Kk,Vv,Entryn) {

    value=v;

    next=n;

    key=k;

    hash=h;

}


(3)计算位置的原理

很多操作都需要先确定一个键值对所在的桶下标。

int hash =hash(key);

int i = indexFor(hash, table.length);


计算 hash 值:

final inthash(Objectk) {

    int h =hashSeed;

    if (0 != h && k instanceofString) {

        returnsun.misc.Hashing.stringHash32((String) k);

    }


    h^= k.hashCode();


    // This function ensures that hashCodes that differ only by

    // constant multiples at each bit position have a bounded

    // number of collisions (approximately 8 at default load factor).

    h^= (h >>> 20) ^ (h >>> 12);

    return h ^ (h >>> 7) ^ (h >>> 4);

}

public final inthashCode() {

    returnObjects.hashCode(key) ^Objects.hashCode(value);

}


取模:

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:

x   : 00010000

x-1 : 00001111

令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:

y       : 10110010

x-1     : 00001111

y&(x-1) : 00000010

这个性质和 y 对 x 取模效果是一样的:

y   : 10110010

x   : 00010000

y%x : 00000010

我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key的 hash 值对桶个数取模:hash%capacity,如果能保证capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。

static int indexFor(int h, intlength) {

    return h & (length-1);

}

HashMap的初始长度是16,并且每次自动扩展或手动初始化时,长度必须是2的幂。

index = HashCode(Key) & (Length - 1)


下面我们以值为“book”的Key来演示整个过程:

1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110

1001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。


为什么长度必须是16或者2的幂?比如HashMap长度是10会怎么样?

假设HashMap的长度是10,重复刚才的运算步骤:

HashCode:1011100011101011101001

Length-1:                   1001

Index:                      1001

单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011:

HashCode:1011100011101011101011

Length-1:                   1001

Index:                      1001

让我们再换一个HashCode 101110001110101110 1111试试:

HashCode:1011100011101011101111

Length-1:                   1001

Index:                      1001

是的,虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!

这样,显然不符合Hash算法均匀分布的原则。

反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。


(4)扩容原理

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。

为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。


和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。

参数                                   含义

capacity                          table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。

size                                 键值对数量。

threshold                         size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。

loadFactor                       装载因子,table 能够使用的比例,threshold = capacity * loadFactor。默认值为0.75f。


衡量HashMap是否进行Resize的条件如下:

HashMap.Size >=threshold= Capacity * LoadFactor

HaashMap的Resize不是简单的把长度扩大,而是经过下面两个步骤:

1. 扩容

创建一个新的Entry空数组,长度是原数组的2倍。

2. ReHash

遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大后,Hash的规则也随之改变。


让我们回顾一下Hash公式:

index = HashCode(Key)&(Length - 1)

当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。

从下面的添加元素代码中可以看出,当需要扩容时,令capacity 为原来的两倍。

void addEntry(inthash,Kkey,Vvalue,intbucketIndex) {

    Entry e =table[bucketIndex];

    table[bucketIndex]= new Entry<>(hash, key, value, e);

    if (size++ >=threshold)

        resize(2 * table.length);

}

扩容使用resize() 实现,需要注意的是,扩容操作同样需要把oldTable 的所有键值对重新插入newTable 中,因此这一步是很费时的。

void resize(intnewCapacity) {

    Entry[] oldTable =table;

    int oldCapacity = oldTable.length;

    if (oldCapacity == MAXIMUM_CAPACITY) {

        threshold=Integer.MAX_VALUE;

        return;

    }

    Entry[] newTable = newEntry[newCapacity];

    transfer(newTable);

    table=newTable;

    threshold= (int)(newCapacity *loadFactor);

}


void transfer(Entry[] newTable) {

    Entry[] src =table;

    int newCapacity = newTable.length;

    for (int j = 0; j < src.length; j++) {

        Entry e =src[j];

        if (e != null) {

            src[j]= null;

            do{

                Entry next = e.next;

                int i = indexFor(e.hash, newCapacity);

                e.next =newTable[i];

                newTable[i]=e;

                e=next;

            }while (e != null);

        }

    }

}


(5)高并发问题

     Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

 public classHashMapInfiniteLoop

private static HashMap

map = new HashMap(2,0.75f); 

public static void main(String[] args)

       map.put(5, "C"); 

       new Thread("Thread1")

           public void run() { 

               map.put(7, "B"); 

               System.out.println(map); 

           }; 

}.start();


       new Thread("Thread2")

           public void run() { 

               map.put(3, "A); 

               System.out.println(map); 

           }; 

       }.start();       

      }

 }

其中,map初始化为一个长度为2的数组,loadFactor=0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行resize。

通过设置断点让线程1和线程2同时debug到transfer方法的首行。注意此时两个线程已经成功添加数据。放开thread1的断点至transfer方法的“Entry next = e.next;” 这一行;然后放开线程2的的断点,让线程2进行resize。结果如下图:

注意,Thread1的e指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表,此时e仍然指向key(3),next指向key(7)。

线程一被调度回来执行,先是执行newTalbe[i] = e,此时newTalbe[i]为key(3),然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)。

e.next = newTable[i]导致key(3).next指向了key(7)。注意:此时的key(7).next 已经指向了key(3),环形链表就这样出现了。

此时,问题还没有直接产生。当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!


如何杜绝这种情况产生呢?

在高并发场景下,我们通常使用另一个集合类ConcurrentHashMap,这个集合类兼顾了线程安全和性能。


(6)HashMap在Java8中的优化

     HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

为什么HashMap要树化呢?

本质上这是一个安全问题,因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表的查询是线性的,会严重影响存取的性能。

JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。

JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

并且 JDK 1.8 的实现也在链表过长时(大于8)会转换为红黑树。


Java8中put方法的原理:

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点  添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。


与 HashTable 的比较

HashTable 使用 synchronized 来进行同步。

HashMap 可以插入键为 null 的 Entry。

HashMap 的迭代器是 fail-fast 迭代器。

HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。


2)LinkedHashMap

   LinkedHashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素。从名字上可以看出该容器是linked list和HashMap的混合体,也就是说它同时满足HashMap和linked list的某些特性。可将LinkedHashMap看作采用linked list增强的HashMap。

事实上LinkedHashMap是HashMap的直接子类,二者唯一的区别是LinkedHashMap在HashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。上图给出了LinkedHashMap的结构图,主体部分跟HashMap完全一样,多了header指向双向链表的头部(是一个哑元),该双向链表的迭代顺序就是entry的插入顺序。

除了可以保迭代历顺序,这种结构还有一个好处:迭代LinkedHashMap时不需要像HashMap那样遍历整个table,而只需要直接遍历header指向的双向链表即可,也就是说LinkedHashMap的迭代时间就只跟entry的个数相关,而跟table的大小无关。

(1)put()方法

     put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于get()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry。

注意,这里的插入有两重含义:

1.从table的角度看,新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插

  法将新的entry插入到冲突链表的头部。

2.从header的角度看,新的entry需要插入到双向链表的尾部。


(2)remove()方法

     remove(Object key)的作用是删除key值对应的entry,该方法的具体逻辑是在removeEntryForKey(Object key)里实现的。removeEntryForKey()方法会首先找到key值对应的entry,然后删除该entry(修改链表的相应引用)。查找过程跟get()方法类似。

注意,这里的删除也有两重含义:

1.从table的角度看,需要将该entry从对应的bucket里删除,如果对应的冲突链表不空,

  需要修改冲突链表的相应引用。

2.从header的角度来看,需要将该entry从双向链表中删除,同时修改链表中前面以及后

  面元素的相应引用。


JDK1.8中的LinkedHashMap:

LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

void afterNodeAccess(Node p) { }

void afterNodeInsertion(boolean evict) { }


afterNodeAccess()

当一个节点被访问时,如果accessOrder 为true,则会将该节点移到链表尾部。也就是说指定为 LRU顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node e) { // move node to last

    LinkedHashMap.Entrylast;

    if (accessOrder && (last = tail) !=e) {

        LinkedHashMap.Entry p =

            (LinkedHashMap.Entry)e, b = p.before, a = p.after;

        p.after = null;

        if (b == null)

            head=a;

        else

            b.after =a;

        if (a != null)

            a.before =b;

        else

            last=b;

        if (last == null)

            head=p;

        else{

            p.before =last;

            last.after =p;

        }

        tail=p;

        ++modCount;

    }

}


afterNodeInsertion()

在 put 等操作之后执行,当removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。evict 只有在构建 Map 的时候才为false,在这里为true。

void afterNodeInsertion(boolean evict) { // possibly remove eldest

    LinkedHashMap.Entryfirst;

    if (evict && (first = head) != null &&removeEldestEntry(first)) {

        Kkey= first.key;

        removeNode(hash(key), key,null, false, true);

    }

}

removeEldestEntry() 默认为 false,如果需要让它为true,需要继承LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected booleanremoveEldestEntry(Map.Entryeldest) {

    return false;

}


LRU缓存

以下是使用LinkedHashMap 实现的一个 LRU缓存:

* 设定最大缓存空间 MAX_ENTRIES 为 3;

* 使用 LinkedHashMap 的构造函数将    accessOrder 设置为true,开启LRU 顺序;

* 覆盖 removeEldestEntry() 方法实现,在节点多于MAX_ENTRIES 就会将最近最久未使用的数据移除。


class LRUCache extends LinkedHashMap{

    private static final int MAX_ENTRIES = 3;

    protected boolean removeEldestEntry(Map.Entryeldest) {

        return size() > MAX_ENTRIES;

    }


    LRUCache() {

        super(MAX_ENTRIES, 0.75f, true);

    }

}


public static void main(String[] args) {

    LRUCache cache = new LRUCache<>();

    cache.put(1, "a");

    cache.put(2, "b");

    cache.put(3, "c");

    cache.get(1);

    cache.put(4, "d");

    System.out.println(cache.keySet());

}

[3, 1, 4]


3)TreeMap:

   TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。

TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度。


4)HashTable:

   HashMap是支持null键和null值的,而HashTable在遇到null时,会抛出NullPointer

Exception异常。这并不是因为HashTable有什么特殊的实现层面的原因导致不能支持null键和null值,这仅仅是因为HashMap在实现时对null做了特殊处理,将null的hashCode值定为了0,从而将其存放在哈希表的第0个bucket中。

HashMap/HashTable内部用Entry数组实现哈希表,而对于映射到同一个哈希桶(数组的同一个位置)的键值对,使用Entry链表来存储(解决hash冲突)。

HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。HashMap默认的初始化大小为16,之后每次扩充为原来的2倍。

HashTable是同步的,HashMap不是,也就是说HashTable在多线程使用的情况下,不需要做额外的同步,而HashMap则不行。


5)WeakHashMap

存储结构

WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。

WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM对这部分缓存进行回收。

private static class Entry extends WeakReference implements Map.Entry

ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。

ConcurrentCache 采取的是分代缓存:

经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);

不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。

当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。

当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。

public final class ConcurrentCache {

    private final intsize;

    private final Mapeden;

    private final Maplongterm;

    public ConcurrentCache(int size) {

        this.size =size;

        this.eden = new ConcurrentHashMap<>(size);

        this.longterm = new WeakHashMap<>(size);

    }


    publicVget(Kk) {

        Vv= this.eden.get(k);

        if (v == null) {

            v= this.longterm.get(k);

            if (v != null)

                this.eden.put(k, v);

        }

        returnv;

    }


    public void put(Kk,Vv) {

        if (this.eden.size() >=size) {

            this.longterm.putAll(this.eden);

            this.eden.clear();

        }

        this.eden.put(k, v);

    }

}


三、Set

   1)HashSet:

HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法


2)TreeSet:

TreeSet是对TreeMap的简单包装,对TreeSet的函数调用都会转换成合适的TreeMap方法


3)LinkedHashSet:

LinkedHashSet是对LinkedHashMap的简单包装,对LinkedHashSet的函数调用都会转换成合适的LinkedHashMap方法


四、Queue

   PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue的底层实现。

Deque接口又有ArrayDeque和LinkedList、ConcurrentLinkedDeque实现类及BlockingDeque接口,其中BlockingDeque接口有LinkedBlockingDeque并发实现类。


Queue的实现

1)没有实现的阻塞接口的LinkedList: 实现了java.util.Queue接口和java.util.AbstractQueue接口

  内置的不阻塞队列: PriorityQueue 和ConcurrentLinkedQueue

  PriorityQueue 和ConcurrentLinkedQueue 类在 Collection

Framework 中加入两个具体集合实现。

  PriorityQueue 类实质上维护了一个有序列表。加入到 Queue 中的元素根据它们的天然排序(通过其 java.util.Comparable 实现)或者根据传递给构造函数的 java.util.Comparator 实现来定位。

  ConcurrentLinkedQueue是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。


2)实现阻塞接口的:

  java.util.concurrent中加入了BlockingQueue 接口和五个阻塞队列类。它实质上就是一种带有一点扭曲的FIFO 数据结构。不是立即从队列中添加或者删除元素,线程执行操作阻塞,直到有空间

或者元素可用。

五个队列所提供的各有不同:

  * ArrayBlockingQueue :一个由数组支持的有界队列。

  * LinkedBlockingQueue :一个由链接节点支持的可选有界队列。

  * PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。

  * DelayQueue :一个由优先级堆支持的、基于时间的调度队列。

  * SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。


Deque的使用场景

在一般情况,不涉及到并发的情况下,有两个实现类,可根据其自身的特性进行选择,分别是:

LinkedList 大小可变的链表双端队列,允许元素为插入null。

ArrayDeque 大下可变的数组双端队列,不允许插入null。

ConcurrentLinkedDeque 大小可变且线程安全的链表双端队列,非阻塞,不允许插入null。

LinkedBlockingDeque 为线程安全的双端队列,在队列为空的情况下,获取操作将会阻塞,直到有元素添加。


注意:LinkedList 和 ArrayDeque 是线程不安全的容器。

你可能感兴趣的:(Java容器相关(2)-- Map、Set、Queue)