Q:cas如果一直失败的解决方案:cas属于自旋锁,在1.6就引入了适应性自旋锁,(1.6是写死的 后面可以改变)通过比较自旋锁时间阈值(上一次该锁的自旋时间 还有当前状态所决定的) 来决定以下操作:超过阈值后 阻塞后续线程的。
Q:concurrenthashmap的实现:
在ConcurrentHashMap中通过一个Node<K,V>[]数组来保存添加到map中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个ConcurrentHashMap对象的话,只是设定了一个sizeCtl变量,这个变量用来判断对象的一些状态和是否需要扩容。
第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。
通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过hash值和数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。
取元素的时候,相对来说比较简单,通过计算hash来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断key和key的hash,取出value值。
就是当数组某一个位置的节点个数大于8个,如果并且数组数目没有超过64的话就进行扩容,但是如果是超过64的话就进行红黑树的转换。扩容的时候一定要保证扩容后3/4.
一般来说是通过Sizectl来判断当前hashmap的状态 阈值 大小是数组长度的3/4
sizectl = -1表示还没有初始化 sizectl = -(1+n)表示正在扩容
下面进行put方法的思考与分析:
- 当添加一个键值对的时候,首先判断保存这些键值对的数组是否初始化了
- 如果没有的话 进行初始化数组
- 然后通过计算对象hash以及数组长度来计算出hash值,来确定放在数组哪一个位置
- 如果这个位置为空则直接添加,如果不是空的话,则取出这个节点来
- 如果取出的节点hash值是-1的话表示当前正在对这个数组进行扩容,将数据复制到新的数组,则当前线程前往帮助
- 最后一种情况就是:啥都不是,通过synchronized来加锁,进行操作。
- 然后判断当前节点是否为树还是链表 -2就是链表
- 如果是链表则遍历整个链表,直到直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,属于同一个键值对,可以进行覆盖,不然的话就加在尾部。
- 如果是树的话 就调用函数puttreevalue方法添加到树中
- 添加完成后 看是否需要转换为树。
下面就是对扩容的详细解释:treeifyBin 在put方法的详解中,我们可以看到,在同一个节点的个数超过8个的时候,会调用treeifyBin方法来看看是扩容还是转化为一棵树
-
当数组长度小于64的时候,扩张数组长度一倍,否则的话把链表转为树
- tryPresize
-
一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出。
- 在tryPresize方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容
下面就是对获取数据的详细解释:
* 相比put方法,get就很单纯了,支持并发操作, * 当key为null的时候回抛出NullPointerException的异常 * get操作通过首先计算key的hash值来确定该元素放在数组的哪个位置 * 然后遍历该位置的所有节点 * 如果不存在的话返回null
扩展学习:首先学习一下CAS CAS全称是compareandswap 在原子包中经常使用CAS来实现原子操作。getandincresment
,cas底层是通过操作系统实现该功能 ,(cmpxchg查询当前处理器数量)从过添加前缀lock实现。
cas缺点:
- 循环时间开销很大:在执行底层函数getandaddint时候可以发现,循环等待执行的时候,会空转消耗cpu的资源。
- 只能操作一个共享变量,cas一次只能操作一个共享变量 对于多个共享变量组合的操作是无法使用cas的
- ABA问题 可以通过变量的版本号来区别 版本号可以写在同一内存中(前后 要后续处理)