java并发编程之List、Set、ConcurrentHasMap
1、ArrayList
- 特点:查询速度快、自动扩容、元素有放入顺序、元素可重复
- 存储结构:数组
- 扩容原理以及扩容大小
- ArrayList再扩容是会扩容为原来的1.5倍
- 在ArrayList调用add()方法的时候,会去判断是否需要进行扩容,并且需要扩容的时候时候是直接新建一个数组,直接对原数组进行拷贝到新数组提升效率。并且扩容的话在1.7和1.8计算方式是也有了一定的变化
- 1.7:(oldLen*3)/2 + 1
- 1.8: oleLen右移一位速度更快
2、LinkedList
- 特点:增删快
- 存储结构:链表
3、HashMap
-
特点:自动扩容、key,value存储,key可以为null、相同key会被覆盖
-
存储结构
- JDK1.7:数组+链表
- JDK1.8:数组+链表+红黑树
-
相关实现原理:
-
我们知道简单的程序=数据结构+算法
-
hashmap使用的算法称之为
散列表
,可以把任意长度值(Key)通过散列算法变换成固定长度的key(地址)通过这个地址进行访问的数据结构它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
-
HashMap采用的Hashcode:通过字符串算出它的ascii码,进行mod(取模),算出哈希表中的下标。
-
但是当多个key进行哈希计算并且取模之后会发现他们取模后的值是一样的这就是
哈希冲突
,那如何解决?- 其实在HashMap中采用链表和数组来存储数据的原因也在此。使用数组是为了让其数据进行哈希取模之后放在一个数组之内,但是多个数据取模之后会出现要放在数组的同一个位置这就到时出现了碰撞,引用链表进来就解决了该问题,只需要判断该处有没有数据,有的话就按照链表存储,没有就直接存放。
-
但是当很多数据进行hash取模之后存放的位置都是同一个下标,虽然有链表来解决这个问题,但是当数据量增多后链表的查询速度会变得很低,哪又如何解决呢?因此在Jdk1.8的时候,在hashMap引入了红黑树来解决这个问题,当链表长度达到转换的阈值是8,并且数组的长度达到64的时候,便会将链表转换为红黑树提高查询的速度。当移除数据时,判断等于退化的阈值时(6),便会将红黑树转换为链表
- 为什么升级为树和降为链表之间存在一个7呢?
- 因为两者直接存在一个差值是为了避免频繁的进行树与链表之间的转换,试想一下当有频繁的数据插入与移除的时候,两个转换阈值之间没有一个差值,便会出现一会转换为树一会转换为链表的情况
- 为什么升级为树和降为链表之间存在一个7呢?
-
-
相关问题思考
-
3.1、HashMap在进行put数据的时候会干些什么呢?
- 在我们进行put数据的时候,会先判断数据是否为空,如果为空的话,便会对数组进行初始化工作,然后会先对key进行hash计算,确定其所放的位置,如果放的位置没有节点,就会新增节点,然后放到先前计算的索引的位置,然后会去判断是否超过扩容阈值,超过的话,就会进行扩容,没有就直接插入,如果要放的位置有节点的话,就会判断头结点的key是否与插入的相同,如果相同的话就会直接覆盖掉原来的值,并返回覆盖前的值,如果不相同的话,就会判断头节点是不是一个红黑数的头节点,是的话**,就会找到红黑树的根节点进行遍历,去判断是不是有相同的key**,有的话也是直接覆盖掉,没有的话,就会新建节点,放到红黑数对应的位置,然后在进行红黑树的插入平衡调整,然后去判断是否超过阈值,超过就扩容,没超过就不扩容,如果不是一个红黑树的话,便会去遍历链表,同时统计他的节点个数,会判断是否有与key相同的节点,有点话就覆盖**,没有的话就会插入到链表的尾部,然后判断他的节点个数是否超过8**,没有的话就会去判断是否超过阈值,超过的话进行扩容,没有便不扩容,如果超过了树阈值-1(7),同时数组的长度也是超过64的便会去转变为红黑树,然后也会去进行判断是否阈值,超过扩容,没超过便不扩容。
-
3.2、HashMap的扩容阈值是多少呢?我们可以进行更改吗?为什么是这个数呢?
-
HashMap的扩容阈值默认是12,他是经过默认容量*扩容因子得到,默认的扩容因子是0.75,在我们new HashMap的时候可以对其进行指定阈值以及初始的容量大小,便可以改变其扩容阈值。HashMap最后选择0.75为扩容阈值的主要原因是在经过大量的泊松分布计算之后,发现0.75作为扩容因子不会导致频繁扩容以及迟迟不扩容的问题。
HashMap<String, String> hashMap = new HashMap<String, String>(10, (float) 0.6); public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
-
-
3.3、HashMap它扩容原理是如何的?如何确定在扩容之后原来的数据应该存放在哪里呢?
-
HashMap进行扩容的时候,首先会判断老数组的长度是否超过了Integeter的最大值(),超过的话,直接结束。没有超过的话,变为原来的2倍,table指向新的数组,会去遍历处理老数组,判断当前遍历的索引位置是否有节点,没有的话继续遍历,有的话会判断当前节点是否只有一个节点,是的话,计算该节点在新表的索引位置,直接放到新表,会判断老数组节点是否处理完,没有完的到话,继续遍历。如果当前不止一个节点,会判断该节点是什么,如果是红黑树的话,会去遍历处理该索引位置的红黑树节点,去判断e.hash & oldCap ==0(节点的hash) 是的话节点就加到lowhead链表的尾部,不是的话,就加到hiHead链表的尾部,然后,lowhead的处理就是,放到原索引位置,红黑树链表节点转换处理,hiHead的处理是,放到原索引+OldCap位置、**红黑树链表节点处理转换,也会去判断是否处理完毕。如果这个这个节点是链表的话,会去遍历处理该索引位置的链表节点,回去判断e.hash & OldCap == 0,是的话加到lowhead链表尾部,不是就加到HiHead链表的尾部,lohead放到原索引位置,hihead放到原索引+oldCap位置,去判断是否处理完毕,没有完毕的会,重复以上操作
-
会进行按位与运算,判断其存放在原有位置,还是原来的位置加上原来的容量长度,一个key的hash和新的容量减1进行与运算,计算之后就会出现两种情况:
- 一个的高位是1
- 一个是0
- 根据高低位不同进行存放:计算后如果高位是0就是原来位置如果高位是1,就是原来的位置+老的容量
进行按位与算,扩容后的容量-1 进行计算 要么是原来的位置要么是原来位置加上老的容量 同一个索引位置进行一次新的与运算 (n-1) & hash 0000 0000 0000 0000 0000 0000 0001 0000 16(老数组容量) 0000 0000 0000 0000 0000 0000 0000 1111 15(n-1) Hash1(key1)1111 1111 1111 1111 0000 1111 0000 0101 某个key的hash ------------------------------------------------------------------------------- 0000 0000 0000 0000 0000 0000 0000 0101 索引是5 0000 0000 0000 0000 0000 0000 0000 1111 15(n-1) Hash2(key2) 1111 1111 1111 1111 0000 1111 0001 0101 某个key的hash ------------------------------------------------------------------------------- 0000 0000 0000 0000 0000 0000 0000 0101 计算出的索引是5 扩容后 16 * 2 = 32 0000 0000 0000 0000 0000 0000 0010 0000 32(新的容量) 0000 0000 0000 0000 0000 0000 0001 1111 31(n- 1) Hash1(key1)1111 1111 1111 1111 0000 1111 0000 0101 某个key的hash ------------------------------------------------------------------------------- 0000 0000 0000 0000 0000 0000 0000 0101 索引位置5 0000 0000 0000 0000 0000 0000 0001 1111 31(n- 1) Hash2(key2)1111 1111 1111 1111 0000 1111 0001 0101 某个key的hash ------------------------------------------------------------------------------- 0000 0000 0000 0000 0000 0000 0001 0101 索引位置21
-
-
3.4、HashMap的扩容倍数为什么是2的n次方,如果输入10会怎样
-
如果数组长度不是2的n次幂,计算出的索引特别容易相同容易发生hash碰撞,导致其余数组,空间上很大程度上没有存储数据。
-
输入10,会进过或运算和右移运算变为2的n次方
通过或运算和右移运算变为2的n次幂, -1 操作,防止恰好给的是2的n次幂 static final int tableSizeFor(int cap) {//int cap = 10 int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } 比如我们输入10 n = cap -1(10 - 1) 9 //第一次右移 n |= n >>> 1; 00000000 00000000 00000000 00001001 //9 | 00000000 00000000 00000000 00000100 //9右移之后变为4 ----------------------------------------------------------- 00000000 00000000 00000000 00001101 //按位异或之后是13 n |= n >>> 2; 00000000 00000000 00000000 00001101 //13 00000000 00000000 00000000 00000011 //13右移两位之后变为3 ----------------------------------------------------------- 00000000 00000000 00000000 00001111 //按位异或之后是15 n |= n >>> 4; 00000000 00000000 00000000 00001111 //15 00000000 00000000 00000000 00000000 //15右移四位之后变为0 ---------------------------------------------------- 00000000 00000000 00000000 00001111 //按位异或之后是15 n = 15 return n+1(16)
-
-
完整的流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wgf1iJY-1679562452780)(.\images\1679558720524.png)]
-
4、HashSet
- 特点:是基于HashMap实现的,构造函数直接new了一个HashMap,hashSet的值是存放在HashMap的key上面,hashmap的value统一为Present。基本都是直接调用HashMap的底层方法实现。
- HashSet的值不允许重复
- HashSet 使用成员对象来计 算 hashcode 值,对于两个对象来说hashcode 可能相同,所以 equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回 false。
5、HashTable
- HashTable与HashMap异同点
- HashMap和HashTable都是实现了map接口
- HashTable是线程安全的,通过synchronized进行加锁,但是这就会导致他的效率会慢,而hashmap线程不是安全的
- HashTable是不允许键值对为空的,而hashmapHashMap,HashMap他会把为null的key给直接放在第一个位置
6、ConcurrentHashMap
-
特点:并发安全、自动扩容
-
存储特点:底层采用数组、链表、红黑树 内部大量采用CAS操作。并发控制使⽤synchronized 和
CAS 来操作来实现的。
-
currentHashMap 数组初始化
- 初始化采用的是CAS + 自旋—>保证线程的安全线程
- 1、先判断前面的sizeCtl是不小于0的,是的话,就是有其他线程正在初始化,
- 2、然后,会进行一个U.compareAndSwapInt(this,SIZRCTL,sc,-1)进行一个判断,修改当前的sizeCtl的值为-1,修改成功成功的话,就会对数组进行初始化,初始化的时候,里面有个计算阈值的操作,他是n-(n>>>2)也就是相当于0.75。失败的话就会自旋等待(一直循环)。最后会将计算好的扩容阈值赋给sizeCtl。----->计算扩容阈值—>很妙,他是n-(n>>>2)也就是相当于0.75
-
Sizectl变量的含义
SizeCtl值 含义 0 表示数组没有进行初始化,且数组初始容量为16 1 如果数组没有初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么记录的就是数组的扩容阈值 -1 表示数组正在进行初始化 小于0并且不是-1时 表示数组正在扩容,-(1+n)表示此时有n个线程正在共同完成数组扩容操作 -
currentHashMap 扩容原理?
-
扩容的时候,首先会去判断cup数是不是大于1的,是的话会给每个线程划分任务(对数组长度右移动3位然后除以cpu数,否则就是直接等于n—三元运算符),然后就判断当前线程是不是扩容线程,是的话就按照两倍扩容新数组,记录线程开始迁移的桶位,然后从后往前开始迁移。会在已经迁移的桶位,用一个fwd节点去占位,表示已经迁移了,这个节点里面的hash值moved—> -1 。如果没有迁移的话,他会进行加锁,用一个代码块(synchronized(f){})将这个桶锁住,进行数据的迁移(具体迁移和hashMap类似的)线程各自只会负责各自的迁移任务
-
多线程协助扩容的操作会在两个地方进行触发
- 1、当添加元素的时,发现添加元素对应的桶位为fwd节点,就会去协助扩容,然后再添加元素
- 2、当添加完元素后,判断当前元素是不是达到了扩容阈值,此时方法sizeCtl的值小于0,并且新数组不为空的时候,就会去协助扩容
-
-
currentHashMap Put原理
-
为什么不支持key或者value为null?
-
value为null,因为concurrentHashMap是多线程,当通过get(key)去获取值得到null,无法判断这个映射是null还是没有找到对应的key而为null,单线程hashmap下可以使用containskey去判断到时是否包含了这个null。还有就是如果允许ConcurrentHashMap允许存放值为null的value的话,这时候有两个线程,T1线程调用get(key)返回null,我们是不知道这个null是没有映射为null还是本来就是null,假如这时就是没有找到对应的key,我们调用ContaintsKey来验证的话期望得到的是false,但是,如果我们在调用get和ContainsKey之间,另一个线程T2执行put(key,null)的操作,那么我们调用ContainsKey返回的就是true,就是与假设不符合。就有了二义性
至于key为什么不能为null,我猜的话是作者doug lee不喜欢null,好像是注释中写到在并发情况下检查空键和值很困难,设计之初就不允许了key为null,具体的话也没找到一个官方的解释
-
-
ConcurrentHashMap的get方法是否需要加锁?
-
get方法是不需要加锁的,因为Node的元素val和指针next都是使用volatile来修饰的,在多线程情行下,线程T1修改了节点的val或者新增节点的时候多线程T2是可见的。
-
Get方法不需要加锁与volatile修饰的哈希桶有关吗?
没有关系,哈希桶Table用volatile来修饰主要是保证数组扩容的时候保证可见性。
-