1.为什么hashmap中链表大小超过8个会自动转化为红黑树,当删除小于6个会重新转换为链表?
根据泊松分布:是一种统计与概率学里常见到的离散概率分布。在负载因子默认为75%时,单个hash槽内元素个数为8的概率小于百万分之一,所以7时个分水岭,等于7时不转换,大于7时转换,小于7时转换为链表。
2.hashma在多线程环境下是不安全的,如何处理这种情况?
- Collections.sysnchronizedMap(Map):创建线程安全的集合
- HashTable
- ConcurrentHashMap
3.Collections.sysnchronizedMap(Map)的实现
sysnchronizedMap内部维护了拥有两个属性:
- 普通的Map对象
- mutex排斥锁
这个类有两个构造方法,若不传递一个mutex参数,则会把mutex赋值为this。
创建完成后,再操作map的时候就会对方法上锁sysnchronized
4.为什么说HashTable效率低?
当HashTable对数据进行操作时都会上锁。
5.HashTable与hashMap的区别?
- HashTable不允许键值为null,hashMap则可以为null
1.因为HashTable在put空值的时候会直接报空指针异常,hashMap则做了特殊处理,默认null会赋值0;
2.HashTable使用安全失败机制(fail-safe),这种机制在读取数据的时候不一定是最新的值。如果你使用null值。就无法判断key是不存在还是为空,因为你无法再调用一次contain(key)来进行判断。
- 实现方式不通,分别集成不同的类:HashTable extends Dictionary、hashMap extends AbstractMap
- 初始化容量不同:HashTable ->11,hashMap ->16
- 扩展机制不同:HashTable-> 2oldSize+1, hashMap -> 2oldSzie
- 迭代器不同:HashTable中的Iterator迭代器是fail-fast,HashTable则不会
6.什么是fail-fast,原理是什么?
- 快速失败是java集合中的一种机制,再用迭代器遍历一个集合的时候,如果对集合对象的内容进行了增加删除修改,则会抛出ConcurrentModificationException
- 迭代器在遍历时直接访问集合中的内容,会使用一个modCount的变量;迭代器每次next遍历下一个元素时会检测这个遍历是否被改变,如果改变了就会抛出异常,终止遍历。
7.fail-safe
JUC(java.util.concurrent)包下面的容器都是安全失败的,可以在多线程并发使用,并发修改
8.ConcurrentHashMap它的数据结构是怎样的?为什么并发度那么高?
- jdk7中数据结构:Segment数组+hashEntry链表
1.segment数组是ConcurrentHashMap的一个内部类
2.HashEntry与hashMap中的差不多,唯一不同点是用了volatile修饰它的数据value以及下一个节点next。
3.并发高的原因:
- 原理上:采用了分段锁技术,其中Segment继承了ReentrantLock。当每个线程访问其中一个Segment时,不会影响其他Segment。理论来说,容器大小有多少,它的Segment就有多少,就可以同时允许对等的线程进行操作,并且保证安全性。
4.put逻辑:
(1)首先第一步会尝试获取锁,如果获取锁失败那么会利用scanAndLockForPut() 自旋获取锁:1.尝试自旋获取锁,2.如果重试次数达到了max_scan_retries则改为阻塞锁获取,保证能获取成功
(2)首先定位到Segment,每个Segment对应这一个HashEntry
数组(3)将当前Segment中的table通过key的hashCode计算出hashEntry的下标,定位到hashEntry中的某一个节点(链表头)
(4)遍历链表
(5)若此时链表为空,则判断是否需要扩容并且把新节点插入到 链表头中,根据key-value形成节点插入链表中。
(6)若不为空,则遍历判断传入的key与当前遍历的key是否相同,相同则覆盖旧的value
(7)最后释放锁
5.get逻辑:
- (1)只需要把key通过hash定位到具体的Segment,再通过hash定位到具体的元素上即可。
- (2)因为hashEntry中的value是被volatile修饰的,保证了内存可见性,每次获取都是最新值。
- (3)因此整个get过程不需要加锁。
- jdk8中抛弃了原有的Segment分段锁,而采用了CAS+synchronized来保证并发安全
把hashEntry变成了node,值和next都被volatile修饰,保证了可用性,并且引入了红黑树,在链表大于一定值的时候会自动转换(默认为8)
1.put逻辑:
(1)根据key计算hashCode
(2)判断是否需要进行初始化。
(3)即当前key定位出来的node。如果为空则表示可以直接写入数据,利用CAS尝试写入,失败则自旋保证成功。
(4)如果当前位置的 hoshcode == moved == -1, 则需要进行扩容。
(5)如果以上都不满足,则利用synchronized锁进入数据。
(6)如果数量大于 treeify_threshold则转换为红黑树。
2.get逻辑:
(1)根据计算出来的hashCode去寻找地址,如果在桶上面有值则直接返回。
(2)如果在红黑树那就按照树的方式找值。
(3)如果还不满足,那就按照链表的方式去循环取值。
9.volatile起什么作用?
- 实现可见性:保证不同线程对被volatile修饰的变量进行操作时的可见性,即一个线程修改某个变量时,这个新值对其他线程来说时立即可见的。
- 实现有序性:禁止进行指令重排序。
- 实现原子性:保证对单次读写的原子性。
10.CAS是什么?
CAS是乐观锁的一种实现方式,是一种轻量级锁,操作如下:
- 1.线程在读取数据不进行加锁
- 2.在写回数据的时,比较原值是否被更改
- 3.若被更改则重新执行一边读取流程
- 4.若未被其他线程修改则返回
这属于一种乐观策略,认为并发操作并不是总发生,适用于多读少些的场景。
11.CAS一定能保证数据没有被其他线程修改过么?
以典型的ABA实例:
- 1.初始值为A
- 2.线程1把A改成B
- 3.同时线程2又把B改成了A
- 4.最终结果是正确的,可以正常返回,但是无法记录其中的一个修改过程。
12.如何解决ABA问题
添加版本号:在修改查询他原来的值的时候,顺便带上一个版本号,每次判断包括原值+版本号,若成功,版本号则+1。
13.jdk1.8升级后为什么synchronized反而使用更多?
synchronized在此之前一直是重量级的锁,但是现在更新采用了锁升级的方式:
- 1.先使用偏向锁优先同一线程然后再次获取锁,若失败。
- 2.升级为CAS轻量级锁,如果失败就短暂自旋,防止线程被系统挂起,若失败,
- 3.最终升级为重量级锁。
因此jdk1.8进行了优化,锁是一步一步升级上去的。