1.HashTable怎么实现线程安全?以及为什么效率低下
HashTable在进行put和get的时候会对整个hash表上锁(synchronized),虽然能保证线程安全,但是效率低下。
2. Hashtable 是不允许键或值为 null ?
Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据
什么是fail-safe: fail-safe:这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。
优点:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。 原文链接:blog.csdn.net/striner/art…
缺点: 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的 。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次【contain(key):是用于单线程的hashMap用来判断是否存在还是空】下来对key是否存在进行判断,ConcurrentHashMap同理
hashtable为什么就不能containKey(key) :一个线程先get(key)再containKey(key),这两个方法的中间时刻,其他线程怎么操作这个key都会可能发生,例如删掉这个key
3.什么是fail-fast:
是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出ConcurrentModificationException异常,终止遍历。作者:why技术链接:juejin.cn/post/684490…
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。作者:why技术链接:juejin.cn/post/684490…
场景: java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改) ,这是一种安全机制。
4.ConcurrentHashMap1.7结构:
Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表
HashEntry是用volatile是修饰的:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
禁止进行指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
优点:它在进行put和get的时候就会先进行segment定位,segment数量就是数组的大小,初始为16.可以允许16个线程同时操作。
缺点: 数组加链表的方式,查询的时候,还得遍历链表,会导致效率很低
5.ConcurrentHashMap1.8结构:
CAS + synchronized来保证并发安全性。同样在节点中把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)
put:
根据 key 计算出 hashcode 。
判断是否需要进行初始化。
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
如果当前位置的
hashcode == MOVED == -1,则需要进行扩容。如果都不满足,则利用 synchronized 锁写入数据。
如果数量>
TREEIFY_THRESHOLD则要转换为红黑树。
什么是CAS:
CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。
CAS 操作的流程大体是,
线程在读取数据时不进行加锁
在准备写回数据时,比较原值是否修改
若未被其他线程修改则写回,若已被修改,则重新执行读取流程
这是一种乐观策略,认为并发操作并不总会发生
CAS和ABA问题:
什么是ABA:
原来是数据是A,第一个线程把值改回了B,
又来了一个线程把值又改回了A
对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。
特殊场景(资金交易):但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯
解决方法: 修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号+1;其实加时间戳也是行的,主要就是为了区别过程。
为什么使用Synchronized(重量级):
首先:synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。
JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的