今天准备对Java的容器知识做一次回顾
ArrayList、LinkList、Set、HashMap、ConcurrentHashMap、ArrayDeque、LinkHashMap与LRU
TreepMap和Hash一致性
先把我能记起来的知识梳理一下
ArrayList
底层是一个数组
具有的特性包括:
- 随机存取,从源代码看实现了RandomAccess这一标识性接口标志它的特性,
- 动态扩容
初始化的时候:
- 无参构造器,初始化的容量是0,在第一次进行add操作的时候,直接扩容至10,之后每次add的时候会检查数组容量,如果容量不够会扩容至之前的1.5倍。
- 有参数构造器,如果指定初始容量大于10的时候,虽然初始化容量还是0,但是首次扩容的数值不一样,如果小于10且大于0的的时候,首次扩容直接会扩充至10。
- 有参数构造器,指定初始化容量是0,此时底层使用的空数组和之前的情况使用的空数组并非同一个。在这种情况下扩容的顺序是0-1-2-3-4-6-9-...。
为什么底层会有两个空数组
程序员在使用0值参数构造器的时候,其目的应该是了解该数组的容量不会太大,这样每次扩容都可以节省空间
LinkList
底层是双向链表,访问的时间复杂度是O(n),删除和移动是常数级的,相较于ArrayList,访问是时间复杂度是常数级的,而删除和一定是O(n)的。
Set
之前的两种数据结构结构是有序不唯一,而Set则是存储数据无序且唯一的数据结构,具体的HashSet的底层实现就是HashMap。所有的key对应的value都是PRESENT
常量
添加的时候如果存在该key值,直接返回false,否则返回true
HashMap
底层是数组+链表或者红黑树
关键的参数:
16
调用无参构造器或者有参数构造器初始化容量小于16的时候,会在第一次插入的时候进行扩容至16。
-
选择16是因为16是一个2的n次幂,而每次扩容的都是之前容量的2倍以保证容量始终的是2的n次幂。
- HashMap是通过取余来确定key值在底层数组的位置,hashcode % capacity
- 为了提高这个访问的速度,就需要一个办法代替取余操作,而当capacity是2的n次方的时候,hashcode & (capacity - 1),进行位运算的值刚好就是取余操作的值
0.75
装载因子。是时间与空间的平衡结果。threshold = capacity * loadFactor,如果size > threshold则需要扩容
如果不设定装载因子,随着size的增大,hash冲突的发生,查找的速度会越来越慢
hash函数
其实对象的Object的hashcode并不是直接使用的,而是高16位与低16位做异或运算,让高位数字参与到hash的过程,以期降低hash冲突发生的可能性。
jdk8的改进
-
当链表长度超过8的时候,链表转成红黑树,为什么要在8这个threshold改变,根据统计学计算,一个hashcode发生了8次hash冲突几乎是不可能的情况,也就是说如果出现了这种情况,有可能是自己重写的hashcode计算方法出现了很严重的问题。在已经发生这个问题的基础上力求可以改善查询速度。
为什么要选择红黑树:
- 针对插入操作引起的不平衡来说,AVL和RBTree调整至平衡都是O(1)的时间复杂度
- 但是在删除的时候,由于AVL的高度平衡性,维护平衡的时间复杂度会有O(logN),而RB-Tree最多只需要3次就可以
- 由于AVL的高度平衡性,插入和删除引起的unbalance的概率更大,如果删除和插入的操作比较多,RB-Tree比AVL更加适合,但如果查找的比较多,AVL的复杂度就会小一些
- 扩容的时候不需要所有值重新插入新的数组,而是计算hashcode & oldCapacity,如果是0则不需要改变位置
ConcurrentHashMap
线程安全的HashMap:使用的是CAS和synchronized一起保证线程安全
那么它的线程安全性到底体现在哪呢
在初始化的时候,使用CAS检查sizeCtl的值
sizeCtl:
- -1,正在初始化
- -N,有N-1个线程在扩容
- 大凡小于0的时候,都会调用Thread.yield(),让步,等再次获得执行权限的时候就不再执行初始化
当sizeCtl>=0的时候
CAS: CompareAndSwapInt(this,SIZECTL,sc,-1),成功,这个值就被改成-1,此时可以进行初始化
以上是第一次使用CAS保证线程安全
在put操作中,存在成为头结点的竞争
- 如果没有发生hash冲突显然是不需要考虑加锁,而是使用cas尝试成为头结点:casTabAt
- 如果发生了hash冲突,需要对头结点加锁,然后后续操作同HashMap。
以上是第二次使用cas和第一次使用synchronized,保证put的线程安全。
为什么有的时候使用cas有的使用syn?
这涉及的悲观锁和乐观锁的特性
- 在使用cas的操作中,并发量一般不是很大,操作不是很频繁,例如初始化和头结点竞争,cas不加锁降低了线程切换的开销
- 在处理hash冲突这种相对的频繁的操作,使用syn可以避免cas自旋带来的cpu的开销。
LinkHashMap与LRU
内存调度中中的页面置换算法,选择最近最少使用页面作为被置换出去的页面。
TreeMap与hash一致性算法
hash一致性算法
传统直接取余寻找服务器的算法有一定缺点
- 比如说原来有#0,#1,#2,三台缓存服务器,按照hashcode存取没有问题,一旦增加服务器或者减少服务器就会导致大量缓存服务器无法使用
现设计环状排列的服务器, 结点数量有 Math.pow(2,32) -1,服务器按照hashcode寻找自己的位置
- 存的时候,按照需要存储的对象的hashcode顺时针找到第一个node,如果该node down了,在找下一个,不影响大局
- 取的时候,除非目标服务器down了才会取不到,而且不影响其他
以上hash环同样存在不均匀的问题
比如缓存服务器相对集中在一段区间,而请求对象的hashcode又集中于其他区间,会造成边缘的服务器负载过大
- 在这种情况下需要增加虚拟结点来平衡分布。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。