ConcurrentHashMap

1.  .为什么要使用ConcurrentHashMap? 

在并发编程中 使用HashMap容易造成死循环的 (在多线程环境中,使用HashMap进行put操作会引起死循环,导致CPU的利用率低下)

而使用线程安全的HashTable效率低下   ConcurrentHashMap是线程安全而且高效的HashMap

HashMap在并发执行put的时候会引起死循环,是因为多线程会导致 ConcurrentHashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next接点永远不为空,就会产生死循环获取Entry

import java.util.HashMap;
import java.util.UUID;

public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        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();
    }

}

2. 为什么HashTable的效率低下?而ConcurrentHashMap是怎么解决的?(锁分段技术)

 2.HashTable容器在激烈的并发环境中表现低下的原因:
*    HashTable使用synchronize来保证线程安全,所有访问HashTable的线程都必须竞争同一把锁,线程1在使用put
*    添加元素时,线程2只能等待线程1操作完,不能put元素 也不能get
*
*    如果容器里面有多把锁,每一把锁用于锁容器其中一部分数据
*    当多线程访问容器里面不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率
*    这就是ConcurrentHashMap的锁分段技术
*    首先将数据分成一段一段的,为每段数据加锁,当一个线程占用锁访问其他一个段数据的时候,
*    其他段的数据也能被其他线程访问

3. ConcurrentHashMap的结构

(1)ConcurrentHashMap类里面有  Segment<K,V> HashEntry<K,V>类

初始化方法 是通过initialCapacity  loadFactor=0.75 concurrencyLevel等几个参数初始化segment数组 segmentShift

segmentMask和每个Segment里的HashEntry数组来实现的

 @SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
      
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
       
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }
1. static final class Segment<K,V> extends ReentrantLock implements Serializable 

2. static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
 static final int DEFAULT_INITIAL_CAPACITY = 16;

  
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

  
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

   
    static final int MAXIMUM_CAPACITY = 1 << 30;

  
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    static final int RETRIES_BEFORE_LOCK = 2;

  
  

(2)ConcurrentHashMap的操作

get:  整个get过程不需要加锁,除非是读到空值才加锁重读

原因是它的get方法里将要使用的共享变量都定义为volatile类型

  public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

put操作: 首先定位segment 然后插入 插入操作经过两步

(1)是否需要扩容: 在插入前先判断 HashEntry数组是否超过容量threshold,超过对其扩容

(注意 HashMap是在插入元素后 判断是否到达容量的,到达了就进行扩容,但是很有可能扩容后没有新的元素插入,这是HashMap就进行了一次无效的扩容)

(2)如何扩容

在扩容时,首先会创建一个容量是原来的2倍的数组,再将原数组的元素进行再散列后插入到新的数组里面,而且ConcurrentHashMap只对某个segment进行扩容(segment的数组长度ssize是通过concurrentLevel计算出来的,散列算法是按位与的算法  所以segments的长度只能是2的N次方)

size操作:如果要统计ConcurrentHashMap里的元素大小,就需要统计Segment里的大小后求和,Segement里的全局变量count是一个volatile, 在多线程环境,count容易变化

ConcurrentHashMap的做法: 先尝试两次不通过锁的方法统计count  如果统计过程count变化了,在采用加锁的方式

如何判断count是否变化了?采用motcount 在进行put remove clean 将modcount+1, 统计size前比较modcount是否变化,从而得知容器是否发生变化

上面可以去看java并发编程的艺术一书

下面转自https://github.com/crossoverJie/Java-Interview/blob/master/MD/ConcurrentHashMap.md

(2)1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。

其中的 val next 都用了 volatile 修饰,保证了可见性。

put 方法

重点来看看 put 函数:

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get 方法

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值