【Java】---Collection集合

一、Collection集合

List集合
在这里插入图片描述
Set集合
在这里插入图片描述
queue集合
在这里插入图片描述
Map接口
在这里插入图片描述

二、常见Collection集合子类

2.1、ArrayList

ArrayList是一个其容量能够动态增长的动态数组。线程不安全。常用于随机访问元素。初始容量为10【只有在添加第一个元素时才会扩展到10】。

—高并发下线程安全问题
(A)在add时可能会发生数据覆盖。
elementData[size++] = e;
多线程下,一个线程A刚赋值完成,还没ssize++,那么另一个线程B会覆盖赋值。
(B)数组扩容会发生数组越界。
在边界场景下,线程A和线程B都是先判断容量是否满,没满就不需要扩容。如刚好容量为10,添加第10个元素,两个线程都判断不需要扩容。接着执行elementData[size++] = e;时,会造成数组越界。

2.2、LinkedList

LinkedList是基于双向链表适用于增删频繁且查询不频繁的场景,线程不安全的且适用于单线程,用LinkedList来实现栈和队列。 没初始容量的概念,因为它是链表可以一直拼接。

—高并发下线程安全问题
如果是通过对象修改linkedList的结构,会造成数据覆盖丢失(如add()方法);
使用Iterator遍历修改linkedList时【如add、next、remove、set】,等会抛出ConcurrentModificationException异常;

解决方法:
使用Collections.synchronizedList();
linkedList替换ConcurrentLinkedQueue

2.3、Vector

Vector类是允许不同类型对象共存的变长数组,在new对象时不指定容量的话,默认为10,相应容量的内存就new好了。容量是可以扩展的,一直到最大容量。是同步的,比ArrayList性能差。

相同处:Vector和ArrayList两者底层的数据存储都使用的Object数组实现,因为是数组实现,所以具有查找快;继承的类实现的接口都是一样的;当两者容量不够时,都会进行对Object数组的扩容。
不同处:构造方法不同;线程的安全性不同,vector是线程安全的,在vector的大多数方法都使用synchronized关键字修饰,arrayList是线程不安全的(可以通过Collections.synchronizedList()实现线程安全);

2.4、Queue

Queue:队列,“先入先出”。

Deque 继承自 Queue,直接实现了它的有 LinkedList, ArayDeque, ConcurrentLinkedDeque 等。
Deque 支持容量受限的双端队列,也支持大小不固定的。一般双端队列大小不确定。
Deque 接口定义了一些从头部和尾部访问元素的方法。比如分别在头部、尾部进行插入、删除、获取元素。

2.4.1、ArrayBlockingQueue

ArrayBlockingQueue :一个由数组支持的有界队列。
ArrayBlockingQueue在构造时需要指定容量, 并可以选择是否需要公平性,如果公平参数被设置true,等待时间最长的线程会优先得到处理(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。通常,公平性会使你在性能上付出代价,只有在的确非常需要的时候再使用它。它是基于数组的阻塞循环队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2.4.2、LinkedBlockingQueue

LinkedBlockingQueue :一个由链接节点支持的可选有界队列。
可以选择指定其最大容量,它是基于链表的队列,此队列按 FIFO(先进先出)排序元素。
当new一个队列时,除非指定容量,否则默认最大容量。
LinkedBlockingQueue和ArrayBlockingQueue区别:Array创建的容量内存是固定的,如果满了会从a[0]开始,而list不会,它是不连续,是通过next地址指向的【但是保证了容量时固定的】。

2.4.3、PriorityBlockingQueue

PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限,但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。另外,往入该队列中的元素要具有比较能力。
默认队列容量为11,默认比较器为null。由于是可扩容的,所以是无界。同样也是数组结构。

2.4.4、DelayQueue

DelayQueue :一个由优先级堆支持的、基于时间的调度队列。
DelayQueue是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就以移除这个元素了。此队列不允许使用 null 元素。

2.4.5、SynchronousQueue

SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。
SynchronousQueue作为阻塞队列的时候,对于每一个take的线程会阻塞直到有一个put的线程放入元素为止,反之亦然。在SynchronousQueue内部没有任何存放元素的能力。
SynchronousQueue支持支持生产者和消费者等待的公平性策略。默认情况下,不能保证生产消费的顺序。如果是公平锁的话可以保证当前第一个队首的线程是等待时间最长的线程,这时可以视SynchronousQueue为一个FIFO队列。

由于SynchronousQueue的支持公平策略和非公平策略,所以底层可能两种数据结构:队列(实现公平策略)和栈(实现非公平策略),队列与栈都是通过链表来实现的。

2.5、CopyOnWriteArrayList

关键组成:
final ReentrantLock lock = new ReentrantLock();
volatile Object[] array; //同步机制

//这个Lock锁是控制add添加、remove删除时同步,在get时候不需要,因为volatile保证了可见性。

2.6、CopyOnWriteArraySet

底层使用的是 private final CopyOnWriteArrayList al;
set是不重复的,所以,在添加数据时,首先会判断是否存在值,不存在了才会使用CopyOnWriteArrayList的添加方法,这样又有Lock锁来保证线程安全。

三、Map接口子类

3.1、HashMap

hashMap是线程不安全,允许一个key为null,多个value为null,无序的插入。HashMap是“数组+链表”数据结构【JDK1.8后,“数组+链表+红黑树”】,默认容量是16【jdk1.7的容量是在初始化时就赋值了;jdk1.8是在put()第一个元素是赋值容量,此时会调用resize()方法】,扩容是按照2次幂扩展。

3.1.1、put()源码解读–jdk1.8

源码阅读的流程如下:
在这里插入图片描述

3.1.2、改进的hash算法

hashMap中使用的hash,并不是Object类的本地方法,而是重写了。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hashCode():是使用Object的native方法,得到32位结果。
hash>>>16:hash值是32位,将高16位往右移动16位,其实就是获得高16位数值。
hash^(hash>>>16):将高16位和低16位进行异或运算【相同才会0,不同为1】。

------这个hash计算,主要是保证离散性和分散性,避免hash冲突。

3.1.3、HashMap如何resize()扩容—jdk1.7

新建一个数组,该数组长度为原数组的2倍;
然后循环遍历原数组的每个键值对,键的hashCode和当前数组长度取&运算得到新数组的位置,然后把该键值对放到对应的位置。
--------这种方法put会造成“环形链表”,get时导致死循环,CPU使用率100%。

1.7的“环形链表”形成原因
多线程时对Map进行Put操作,觉察到内存不够,进行扩容,多个线程同时resize()时会出现“环形链表”问题。
在这里插入图片描述
遍历老数组,确定数据复制到新数组时,会改变链表中元素顺序,采用“头部插入法”。现有“A-B”需要复制到新数组。
线程1和线程2同时进行rehash,当线程1加入了1-A还未加入3-C,此时线程2才进入完成rehash,新数组中已经是“3-C-- 1-A”。这时候线程1打算加入3-C,加入成功后,线程1发现3-C节点后还有1-A节点,那么继续头插入法,这样就形成了一个环形。

3.1.4、HashMap如何resize()扩容—jdk1.8

遍历键值对时,旧的元素位置在新数组的原位置,或者放到“原数组+原数组长度”的位置。
在这里插入图片描述

3.1.5、问题解答

问题1:为什么HashMap容量一定要为2的幂?
hash计算已经采用了改进的算法,能保证离散性,避免了hash冲突。有公式hash&(n-1),这样n为2次幂,能够更好的保证“避免hash冲突”。

问题2:为什么加载因子是0.75?
有hash冲突,就会有链表(或红黑树),通过大量模拟计算发现,0.75这个值能得到时间和空间上最优。

问题3:链表长度达到8,一定转换为红黑树?
如果链表长度达到了8,但整个Map数组大小<64,会扩容;只有长度达到了8且整个Map数组大小>=64时,才会转换为红黑树。

问题4:hashMap什么时候扩容?
tab数组为null,会默认16容量进行扩容;
Tab数组某个“桶”下链表长度达到了8但整个Map数组长度<64,会扩容,按照2旧值容量长度 来扩容;
实际存储个数超过capacity
loadFactor时,会扩容;

问题5:为什么经过rehash后,元素的位置要么是在原位置,要么是在原位置加原数组长度的位置?
有(n-1)&hash计算,首先HashMap的容量必定是2次幂。对于容量扩展两倍后,n-1的二进制唯一区别就是多了一个高位1。因此,hash进行&运算,与新加的高位对应的hash值的位置,如果是0,那么就是“原位置”,如果是1,那么就是“原位置+原数组长度”。

问题6:jdk1.8下hashMap多线程安全么?
多线程Put时会导致数据不一致;
一个线程迭代器迭代,另一个线程做插入删除操作,会造成fast-fail;
并发下扩容会丢失数据;
多个线程检测到元素超了,会同时进行扩容,都进行重新计算元素和复制数据,但最终只有一个线程扩容后的数组和复制给tab数组,所以其他线程数据会丢失。

3.2、HashTable

Jdk8中是初始化容量是11。线程安全【加了synchronized关键字】,无序。key和value的值均不允许为null。
当实际存储个数 >= 总容量loadFactor;需要扩容,按照 “2原始容量+1”方式。

--------------------hashTable结构和HashMap结构一样,常见的add、get、remove操作,hashtable加了synchronized关键字,并且在add、get、remove操作上, hashtable就是常见的“数组+单链表”的操作,而hashMap在jdk8版本后用了“数组+链表+红黑树”。

3.3、ConcurrentHashMap

数据结构:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树【当一个hash桶对应的个数达到阈值,一般为8,链表就变成红黑树】,红黑树是为了提高查找效率。Key和value不能为null。

3.3.1、jdk1.7的ConcurrentHashMap

“segment数组+HashEntry数组+链表”
内部进行了segment分段,每个segment继承了ReentrantLock,相当于每个segment是一个锁。总共有0~16个segment,他们直接锁是互不影响。
每个segment内部类似HashTable结构【数组+链表】,value和next用volatile修饰,能保证可见性。
Segment的并发度可以设置,一般是>= 设置值的最小2次幂;设置的过小,会有锁竞争问题。设置的过大,就类似HashTable了,CPU命中率会下降,引起程序性能下降。
除第一个segment外,剩余segment采用延迟初始化的机制。每次Put前,首先检查segment是否为null,如果是就创建segment。

3.3.2、jdk1.8的ConcurrentHashMap

Jdk1.8,采用了 CAS + synchronized 来保证并发安全性,“Node数组+链表+红黑树”。
在put()方法中,如果某个index下元素为null,采用CAS将值直接插入;如果不为Null,接下来用synchronized关键字【只锁这个索引的位置】加锁。当前索引下是链表[元素个数小于8],直接在链表下加入值;当前索引下是红黑树[元素个数大于8],红黑树下插入数据。

Get操作没加锁,怎么保持数据正确?
数据结构的Node区别HashMap,value和next加了volatile关键字,是一种同步机制。

3.3.3、jdk1.8版本put()流程解读

在这里插入图片描述
----------上面有个问题,在添加之后,如果是“链表要转换为红黑树时且tab数组长度<64”,会有进行扩容tryPresize()。----->扩展到>=(size*1.5+1)的2次幂。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DreamBoy_W.W.Y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值