ArryList
ArrayList 底层基于数组实现容量大小动态可变。 扩容机制为首先扩容为原始容量的 1.5 倍。如果1.5倍太小的话,则将我们所需的容量大小赋值给 newCapacity,如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE
来扩容。 扩容之后是通过数组的拷贝来确保元素的准确性的,所以尽可能减少扩容操作。 ArrayList 的最大存储能力:Integer.MAX_VALUE。 size 为集合中存储的元素的个数。elementData.length 为数组长度,表示最多可以存储多少个元素。 如果需要边遍历边 remove ,必须使用 iterator。且 remove 之前必须先 next,next 之后只能用一次 remove。
安全
- 1、List list = new Vector<>();
- 2、List list = Collections.synchronizedList(new ArrayList<> ());
- 3、List list = new CopyOnWriteArrayList<>();
HashMap
区别:
- 头插尾插,
- 还有扩容机制17就是与上新的大小减一,1.8就是遇上大小,0不动,1后移。
- 红黑树
- 空键处理就是1.7直接判断key为空放在0号位,1.8在hash函数判断的key为空hash为0;
hash函数:
1.7,异或了好几次,就为了更加的散列,
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
1.8就是实现了红红黑树,对散列要求低了,就是判断key是否为空右移16位抑或一下,保留了高十六位不变,后十六位就是
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
//默认初始化化容量,即16 1 << 4;
//最大容量,即2的30次方 << 30;
//默认装载因子 0.75f;
entry:k v hash
put()添加
1.创建的时候不会分配,在put第一个值才分配大小,默认16,指定容量后,默认是大于的2的次方,计算的就是
highestOneBit:拿到最高位二进制
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
因为他在找table放时是hashcode与上15,,假如不是全一,1,2都在第0位造成hash碰撞增多,链表很长
2.hash算法算出hash,然后再定位桶;
1.7是与数组长度减一,因为是多了一位,原来有零有一,1.8是与数组长度,0就不动,1,就后面去
然后就是找有没有这个key遍历,找到就直接覆盖,put返回旧的值
没有的话就加一个 ,
加entry
当size超过临界阈值threshold=容量 x 加载因子,扩容
直接进行创建插入头插
再说那个扩容
resize();
主要是
1.transfer()老表数据拷贝到新表出现多线程死循环,因为是尾插法,有两个临时指针,AB两个进行扩容,那个entry的链表尾插A复制过来就是倒叙的了,B县城就不会意识到,再去拷贝头尾相连,死循环,1.8是尾查法,是顺序的
2.更新table = newTable;//2.修改HashMap的底层数组
3.更新阈值(记得和最大值减一比较)就是新的大小 乘 扩展因子
get:
获取key的Hash值 ,再遍历entry;
1.8,就是传putval()函数hash
hash, K key, V value, boolean onlyIfAbsent, boolean evict
两个是是否覆盖旧的值
1.table[]是否为空
2.判断table[i]处是否插入过值
3.判断链表长度是否大于8,如果大于就转换为红黑二叉树,并插入树中
4.判断key是否和原有key相同,如果相同就覆盖原有key的value,并返回原有value
5.如果key不相同,就插入一个key,记录结构变化一次
hash冲突:
- 开放定址法:
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入 - 链地址法
- 再哈希法:
再哈希法又叫双哈希法,有多个不同的Hash函数 - 建立公共溢出区:
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表
ConcurrentHashMap
put实现
1.7 put
其中Segment
在实现上继承了ReentrantLock
,这样就自带了锁的功能。
当执行put
方法插入数据时,根据key的hash值,在Segment
数组中找到相应的位置,如果相应位置的Segment
还未初始化,则通过CAS进行赋值,接着执行Segment
对象的put
方法通过加锁机制插入数据,实现如下:
场景:线程A和线程B同时执行相同Segment
对象的put
方法
1、线程A执行tryLock()
方法成功获取锁,则把HashEntry
对象插入到相应的位置;
2、线程B获取锁失败,则执行scanAndLockForPut()
方法,在scanAndLockForPut
方法中,会通过重复执行tryLock()
方法尝试获取锁,在多处理器环境下,重复次数为64,单处理器重复次数为1,当执行tryLock()
方法的次数超过上限时,则执行lock()
方法挂起线程B;
3、当线程A执行完插入操作时,会通过unlock()
方法释放锁,接着唤醒线程B继续执行;
1.8 put
数据结构
1.8中放弃了Segment
臃肿的设计,取而代之的是采用Node
+ CAS
+ Synchronized
来保证并发安全进行实现,结构如下:
只有在执行第一次put
方法时才会调用initTable()
初始化Node
数组,实现如下:
put实现
当执行put
方法插入数据时,根据key的hash值,在Node
数组中找到相应的位置,实现如下:
1、如果相应位置的Node
还未初始化,则通过CAS插入相应的数据;
2、如果相应位置的Node
不为空,且当前该节点不处于移动状态,则对该节点加synchronized
锁,如果该节点的hash
不小于0,则遍历链表更新节点或插入新节点;
3、如果该节点是TreeBin
类型的节点,说明是红黑树结构,则通过putTreeVal
方法往红黑树中插入节点;
4、如果binCount
不为0,说明put
操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin
方法转化为红黑树,如果oldVal
不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
5、如果插入的是一个新节点,则执行addCount()
方法尝试更新元素个数baseCount
;
size
1.7size
先采用不加锁的方式,连续计算元素的个数,最多计算3次:
1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2、如果前后两次计算结果都不同,则给每个Segment
进行加锁,再计算一次元素的个数;
1.8size
1.8中使用一个volatile
类型的变量baseCount
记录元素的个数,当插入新数据或则删除数据时,会通过addCount()
方法更新baseCount
,实现如下:
1、初始化时counterCells
为空,在并发量很高时,如果存在两个线程同时执行CAS
修改baseCount
值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell
记录元素个数的变化;
2、如果CounterCell
数组counterCells
为空,调用fullAddCount()
方法进行初始化,并插入对应的记录数,通过CAS
设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell
数组,实现如下:
3、如果通过CAS
设置cellsBusy字段失败的话,则继续尝试通过CAS
修改baseCount
字段,如果修改baseCount
字段成功的话,就退出循环,否则继续循环插入CounterCell
对象;
所以在1.8中的size
实现比1.7简单多,因为元素个数保存baseCount
中,部分元素的变化个数保存在CounterCell
数组中,实现如下:
通过累加baseCount
和CounterCell
数组中的数量,即可得到元素的总个数;
HashMap 和 Hashtable 的区别
-
线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); -
效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
-
对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
因为hashtable有key.hashcode()的操作,空对象没法。
-
初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的
tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 -
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。