HashMap和ConcurrentHashMap区别

ConcurrentHashMap在JDK1.7采用分段锁实现线程安全,JDK1.8则使用CAS和synchronized优化,降低锁粒度,提高查询性能。其内部节点包括Node、TreeNode和ForwardingNode,用于处理冲突、树化和扩容。在并发场景下,利用volatile和CAS保证数据一致性。
摘要由CSDN通过智能技术生成

HashMap是线程不安全的,而线程安全类Hashtable只是简单的在方法上加锁实现线程安全,效率低下,所以通常使用ConcurrentHashMap

ConcurrentHashMap不允许空键值对,否则异常;

1.ConcurrentHashMap怎样做到线程安全的?

可以通过减少锁竞争来优化并发性能,而ConcurrentHashMap则在JDK8-使用了锁分段(减小锁范围)、JDK8开始大量使用CAS(乐观锁,减小上下文切换开销,无阻塞)和少量的同步代码块技术

Jdk1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

Segment本身是基于ReentrantLock实现的加锁和释放锁的操作,这样就能保证多个线程同时访问ConcurrentHashMap时,同一时间只有一个线程能操作相应的节点,这样就保证了ConcurrentHashMap的线程安全了。也就是说ConcurrentHashMap的线程安全是建立在Segment 加锁的基础上的,所以我们把它称之为分段锁。

总结

ConcurrentHashMap 在 JDK 1.7 时使用的是数据加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。

节点类型

Node节点

static class Node<K,V> implements Map.Entry<K,V>{

final int hash;

final K key;

volatile V val;

volatile Node<K,V> next;

}

默认桶上的节点就是Node结点。Node只有一个next指针,是一个单链表,提供find方法实现链表查询

当出现Hash冲突时,Node节点会首先以链表的形式链接到table上,当结点数量超过一定数目时,链表会转化为红黑树。

TreeNode节点

static final class TreeNode<K,V> extends Node <K,V>{

TreeNode<K,V> root;

volatile TreeNode<K,V> first;

volatile Thread waiter;

volatile int lockState;

static final int WRITER=1;

static final int WAITER=2;

static final int READER=4;

}

TreeBin 会直接链接到table[i] 一桶上面,该节点提供了一系列红黑树相关的操作,以及加锁、解锁操作。

另外TreeBin提供了一系列的操作

TreeBin(TreeNode<K,V> b),将以b为头结点的链表转换为红黑树

lockRoot(),对红黑树的根节点加写锁

unlockRoot(),释放写锁

find(int h,Object k), 从根结点开始遍历查找,找到相等的节点就返回它,没找到就返回null,当存在写锁时,以链表方式进行查找,不阻塞读锁。

ForwardingNode

static final class ForwardingNode<K,V> extends Node<K,V>{

final Node<K,V>[] nextTable;

}

ForwardingNode在table扩容时使用,内部记录了扩容后的table,即nextTable中,在原table 的槽内放置一个ForwardingNode

ForwardingNode是一种临时节点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据

如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode

ForwardingNode是一种临时结点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode读操作碰到ForwardingNode时,将操作转发到扩容后的新table数组上去执行;写操作碰见它时,则尝试帮助扩容,扩容是支持多线程一起扩容。

ReservationNode保留结点

static final class ReservationNode<K,V> extends Node<K,V>{

ReservationNode(){

super(RESERVED,null,null);

}

Node<K,V> find(int h,Object k){

return null;

}}

在并发场景下、在从Key不存在到插入的时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全

hash值固定为-3,不保存实际数据。只在computeIfAbsent和compute这两个函数式API中充当占位符加锁使用

总结:

就算有多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定 到底是哪个线程有资格进行初始化,其他线程均只能等待。

用到的并发技巧:

  • volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。

  • CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值