Concurrenthashmap知识点
https://blog.csdn.net/a979331856/article/details/105922069
线程安全类的比较:
1> Hashtable:本身比较低效,它的实现基本就是把put、get等方法加上synchronized锁。这就导致了所有的并发操作全都要竞争同一把锁,降低了并发场景下的效率。
2> Collections.synchronized:它的实现没有锁方法,是相当于锁代码块的形式,并发场景下的效率跟hashtable没什么区别。
3> Concurrenthashmap:1.7中,是由segment数组和Entry数组构成的,每个segment相当于一个段,里边的每个元素就是一个entry数组,entry数组的每个元素就是具体的键值对,里边的每个元素都有一个next指针。put的时候,经过hash运算,可能会形成一个链表结构。所以1.7的concurrenthashmap是由数组+链表构成,另外一点就是在对entry数组做修改的时候,必须首先获取与之对应的segment锁,这就是分段锁,减小并发场景下锁的范围,提高效率。1.8中,concurrenthashmap选择了与hashmap相同的数组+链表+红黑树的结构,在锁的实现上,是锁住entry数组的每个元素上,也就是锁住了链表的头结点上,不会影响table元素的读写,范围更小,效率更高。
1.8中concurrenthashmap为什么采用synchronized,而不是其他锁呢:在jdk1.8中,synchnized得到了优化,另外,synchronized涉及到了一个锁升级的过程,无锁->偏向锁->轻量级锁->重量级锁 一步步转换,并不是我们认为的就是重量级锁。
https://blog.csdn.net/F_Hello_World/article/details/104666734
Synchronized的锁升级过程:当一个对象被创建存储在内存里边会涉及到一个对象头,里边有一块区域叫mark word,会保存一个无锁的状态。在一个线程访问到被synchronized修饰的代码块时,会将该线程的id保存到到mark world相应的区域,同时更改锁的状态为偏向锁。
当出现了锁竞争的情况下,会看下当前线程还持有该锁不,若没有,就重新上偏向锁,若还是持有偏向锁,就将偏向锁转化为轻量级锁,并且这种锁的升级是不可逆的。因为这种转化会stop the world,因此对于一些多线程场景较多的应用,可以设置jvm参数,让应用不使用偏向锁。
轻量级锁有两种:自旋锁,自适应自旋锁。对于自旋锁就是一个线程正持有该锁,另外一个线程进来了就等着,相当于执行一个循环,只到成功获取到了锁。但是循环是要消耗资源的,不可能让线程在那一直循环,就设置了一个固定的循环次数10,超过了10回就认为你获取不到该锁了,执行下一步操作,比如升级成重量级锁。对于自适应自旋锁,没有固定的循环次数,而是对于每个锁有一个学习能力,就是说对于某个锁,一个线程循环了5次拿到了,那么下一个线程也循环5次去获取这个锁,获取不到,就直接转成重量级锁。
重量级锁是通过一个监视器锁对象monitor实现的,monitor随着对象的创建而创建。线程获取锁可以理解为获取monitor,成功之后会对monitor对象加一,释放掉了就减一。这种加一减一实际上是通过操作系统的mutex(互斥)、lock实现的。操作系统实现mutex、lock需要从用户态转换成核心态,成本比较大,耗费时间长。这就是synchronized效率低的原因。1.6之前,synchronized主要是依靠操作系统的mutex、lock指令,比较耗费资源,之后引入了偏向锁和轻量级锁的概念,提高了效率,这也是1.8中,concurrenthashmap使用synchronized锁的原因。
为什么concurrenthashmap中的key和value不能为空:concurrenthashmap和hashtable都是支持并发的,这样,在get(key)结果为null的时候,就无法判断是key值对应的value是null,还是这个key值没有做过映射。HashMap是非并发的,可以通过contains(key)来做这个判断。而支持并发的Map在调用m.contains(key)和m.get(key),m可能已经不同了。
Java中快速失败和安全失败:快速失败就是在迭代遍历集合对象时,另外一个线程对该集合做了修改,增加,删除的操作,会直接抛出Concurrent Modification Exception异常。是因为一个线程迭代集合时,会依赖一个modcount变量,它是一个统计该集合被修改次数的变量。在线程迭代遍历时,若modcount不等于期待的值,就会抛出异常。
安全失败的意思是线程迭代集合时,会将原先的集合复制出来一个copy集合,然后迭代copy出来的集合,因此无论别的线程对集合做什么修改是不会影响迭代操作的。但是并发场景下造成的影响就是迭代的集合可能不是最新的集合,另外copy集合也造成了资源的损失。
java.util包下除了Java.util.concurrent是安全失败,其余集合类都是快速失败。
Concurrenthashmap是get的时候如何保证线程安全的:存储元素的entry对象中value属性是加了volatile关键字修饰的,根据JMM的happen before原则,写操作一定在读操作之前,保证了value值的内存可见性。
Concurrenthashmap的put方法过程:最开始是检查key值对应的桶是否初始化,然后判断桶内是否已经有元素,若没有元素的话,就用CAS的方式添加元素,当然可能会有添加失败的情况,就采用自旋的方式只到添加成功。若有元素,则先判断第一个元素的hash值是否是MOVED(-1),若是,证明集合正在扩容,那么该线程就帮助扩容(把整个数组分块交给线程扩容,会记录下来扩容的进度,及每个线程负责的区域,只到扩容结束)。若不等于MOVED,就将桶中的第一个元素上锁,之后再判断第一个元素是否改变(因为上锁的过程中可能其他线程对其做了改变),发生了改变就从头再来,若没有发生改变,当第一个元素的hash值大于0,说明是链表节点,就遍历链表,添加或覆盖key值对应的value,添加成功过后,判断链表是否大于8,若大于8,进行链表转红黑树的操作。同时给数组元素计数。若小于0且不等于MOVED(-1),执行红黑树添加操作。
Concurrenthashmap的get方法过程:根据key值定位到具体的桶,若没有数据,直接返回null。若桶内有数据,且第一个元素刚好是要get的key,就直接返回;若第一个的元素的hash值小于0,说明可能正在扩容,或存储数据的结构为树,调用Node类的find方法查找;否则为链表结构,循环遍历桶内节点去get(key).
hash值大于0说明是链表节点,小于0说明在在迁移或者是树。
Concurrenthashmap的size计数方法过程:主要是通过baseCount变量来记录集合中元素的个数。当执行了put或remove方法之后,会首先通过CAS更改baseCount的值。并发场景下,可能会更改失败,这个时候,会将baseCount的更改保存到一个叫CountCell数组中,CountCell对象中有一个被volaitle修饰的变量value,保存成功后,再遍历数组,求和更新baseCount值。