ConcurrentHashMap讲解

什么是ConcurrentHashMap

ConcurrentHashMap是线程安全且高效的HashMap

为什么要使用ConcurrentHashMap

  • HashMap可能导致程序死循环
  • 线程安全的HashTable效率非常低下

线程不安全的HashMap

下面代码使用HashMap进行put操作会引发死循环,CPU利用率接近100%,所以不能使用HashMap

 final HashMap<String, String> map = new HashMap<String, String>(2);
  Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        map.put(UUID.randomUUID().toString(), "");
                    }
                }, "ftf" + i).start();
            }
        }
    }, "ftf");
    t.start();
    t.join();
}

多线程会导致 HashMap 的Entry 链表形成环形数据结构,一旦形成环形数据结构,Entry 的 next 节点永远不为空,就会产生死循环获取 Entry

效率低下的HashTable

HashTable使用synchronized来保证线程安全,但是在多个线程下,HashTable的效率十分低下。当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。
如线程 1 使用 put 进行元素添加,线程 2 不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低

JDK1.7

ConcurrentHashMap 的锁分段技术可有效提升并发访问率

HashTable的线程都必须竞争同一把锁,所以效率低下,而在ConcurrentHashMap中,数据会被分段存储,每一段都有一把锁,当一个线程访问某段数据时,其他线程也可以访问其他段的数据。

结构

是由 Segment 数组和 HashEntry 数组组成。Segment 是一种可重入锁,扮演锁的角色,HashEntry用于存储键值对,Segment 数组的结构和HashMap类似,由数组和链表构成,一个ConcurrentHashMap 中有一个Segment数组 ,一个Segment 中有一个HashEntry数组,HashEntry是一种链表数据结构,Segment 守护着HashEntry中的元素,当一个线程想对HashEntry中的数据进行修改时,必须先获得Segment锁
在这里插入图片描述
在这里插入图片描述

初始化

通过一些参数来初始化ConcurrentHashMap 中的segment数组、段偏移量segmentShift、段掩码segmentMask和HashEntry数组来实现

  • 初始化 segments 数组
    通过concurrencyLevel变量计算出ssize用来规定segments 数组的长度
if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1; }
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
  • 初始化segmentShift和segmentMask
    要在定位 segment 时的散列算法里会使用到这两个变量,都是由concurrencyLevel变量通过计算得来的
  • 初始化每个segment
    由initialCapacity参数(初始容量)和loadfactor参数(负载因子)计算并初始化每个segment
if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
    ++c;
int cap = 1;
while (cap < c) cap <<= 1;
for (int i = 0; i < this.segments.length; ++i) {
    this.segments[i] = new Segment<K, V>(cap, loadFactor);
}

定位 Segment

因为ConcurrentHashMap使用分段锁,所以当线程在插入或者获取数据时,要先通过散列算法定位到Segment

private static int hash(int h) {
    h += (h << 15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h << 3);
    h ^= (h >>> 6);
    h += (h << 2) + (h << 14);
    return h ^ (h >>> 16);
}

操作

put操作:
因为put会引发写操作,所以必须加锁,首选定位到Segment,然后在Segment中进行插入操作,插入操作分两步,第一步需要判断是否需要对Segment中的HashEntry数组进行扩容,第二步是定位到元素的位置,然后放到HashEntry数组中

get操作:
先经过一次散列,然后使用散列值通过散列算法定位到Segment,再通过散列算法定位到元素

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}

get过程不需要加锁,只有读到空值才加锁重读,原因在于get方法用到的共享变量都是volatile类型的,可以保证所有线程的可见性,不会读取到旧的值,在get方法中只需要读共享变量 count 和 value,所以不需要加锁

transient volatile int count;
volatile V value;

size操作:
需要统计所有Segment中的元素大小并求和,为了线程安全,需要把所有 Segment 的 put、remove 和 clean 方法全部锁住然后累加所有Segment中的count,但是因为发生变化的几率很小,所以会先进行两次不锁Segment 方法的方式来统计size,如果count发生变化(使用另外一个变量mCount来判断是否发生变化),就采用加锁的方式统计size

JDK1.8

结构

在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

JDK1.8为什么把可重入锁ReentrantLock换为了synchronized

  • 在JDK1.6中,对synchronized进行了大量优化,有了锁升级
  • 减少内存开销,如果使用ReentrantLock的话,每个节点都需要通过继承AQS来获得同步支持,但是并不是所有节点都需要获得同步支持的,只需要让链表的头结点或红黑树的根节点同步即可,synchronized就可以只锁住头节点或根节点

操作

put操作:

  • 根据key计算出Hash值
  • 判断是否需要初始化
  • 定位到某个索引位置,判断首节点,如果为null,尝试使用cas的方式添加节点,如果首节点的hash = MOVED = -1,说明其他线程正在扩容,该节点也一起参与扩容,如果上面的条件都不满足,就使用synchronized锁住首节点,判断是链表还是红黑树,遍历插入
  • 当链表长度大于8时,数组扩容或者转化为红黑树

get操作:

  • 计算出key的Hash值,定位到索引位置
  • 如果首节点就是要get的结点,直接返回
  • 如果是链表结构,遍历链表
  • 如果是红黑树,在红黑树中查询

get过程不需要加锁,因为 Node 的元素 value 和指针next 是用volatile 修饰的,每次查询的都是最新值

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值