HashMap、HashTable、JDK1.7 ConcurrentHashMap源码分析 比较

众所周知,哈希表是非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
  HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,利用率可能达到100%,所以,在并发环境中使用HashMap是非常危险的。
  HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。一把大锁锁所有
  在这里插入图片描述
ConcurrentHashMap,数据结构如图,数组+数组+链表,
在这里插入图片描述
使用了分段锁技术将数据分成一段一段来存储,每一段都有一把ReentranLock(重入锁)锁,,当,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问

//ConcurrentHashMap继承AbstractMap
实现ConcurrentMap接口 既有map属性和多线程的属性

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;

    //初始容量
    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;

    //最小的segment数量
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    //最大的segment数量
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    //在锁定之前,大小和containsValue方法中的不同步重试次数。
	//如果表进行了连续修改,将无法获得准确的结果,
	//则使用此方法可以避免无界重试。
    static final int RETRIES_BEFORE_LOCK = 2;

concurrentHashMap的put操作 分为两步:
1、定位到segment
2、判断是否需要对segment中的HashEntry数组进行扩容,然后再在segment中进行插入操作

@SuppressWarnings("unchecked")
public V put(K key, V value) {
//
    Segment<K,V> s; 
	//concurrentHashMap中key和value都不能为null,会抛出空指针异常
    if (value == null)
        throw new NullPointerException();
		//对key进行hash,
    int hash = hash(key);
	//先是无符号右移 segmentShift 位   再   & segmentMask  得到j的值
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        //这一步是找到插入的位置 下面将其介绍此方法
		s = ensureSegment(j);
		// 将键值对保存到对应的segment中
    return s.put(key, hash, value, false); 
}

ensureSegment方法
ensureSegment用于确定指定的Segment是否存在,不存在则会创建
使用getObjectVolatile()方法提供的原子读语义获取指定Segment,如果为空,以构造ConcurrentHashMap对象时创建的Segment为模板,创建新的Segment。ensureSegment在创建Segment期间为不断使用getObjectVolatile()检查指定Segment是否为空,防止其他线程已经创建了Segment。

 private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    //判断Segment的getObjectVolatile()是否为空,为空进行创建新的segment
    //创建Segment期间为不断使用getObjectVolatile()检查指定Segment是否为空,
    //防止其他线程已经创建了Segment
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

HashEntry:
HashEntry将value和next声明为volatile ,是为了保证内存可见性,也就是每次读取都将从内存读取最新的值,而不会从缓存读取。同时,写入next域也使用volatile写入语义保证原子性。写入使用原子性操作,读取使用volatile,从而保证了多线程访问的线程安全性。

 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;
    }
    
    final void setNext(HashEntry<K,V> n) {
        UNSAFE.putOrderedObject(this, nextOffset, n);
    }
    
    static final sun.misc.Unsafe UNSAFE;
    static final long nextOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = HashEntry.class;
            nextOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

put的第一步走完,已经找到位置。

Segment中的put方法:
在这里插入图片描述

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //对其进行尝试性加锁 失败就进行 scanAndLockForPut操作,
    //这个方法后会返回hashEntry节点,(要么有一个要么是重新创建一个出来)
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//下面介绍这个方法
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                //先获取需要put的<k,v>对在当前这个segment中对应的链表的表头结点。
                HashEntry<K,V> first = entryAt(tab, index);
                //遍历first的头节点的链表
                for (HashEntry<K,V> e = first;;)  {
                    //e不为空,说明有hash冲突
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;//新值覆盖旧值
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;//未进入上面的循环,e对应的key不是想要的,进行下一个
                    }
                    else {//e为空
                        if (node != null)//判断是否等于null 不等于就直接进行放值
                            node.setNext(first);
                        else//有可能这是第一次尝试性获取锁成功,所以给他一个new个节点
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        //判断它是不是大于它的阈值,大于进行扩容操作
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        //new出来一个新的hashEntry数组,对他们进行重哈希,这是二倍扩容
                        //扩容完成后将这个节点插入进去
                            rehash(node);
                        else
                        //无需扩容将其直接插入指定的index中进去
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

scanAndLockForPut
在这里插入图片描述

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
   HashEntry<K,V> first = entryForHash(this, hash);
   HashEntry<K,V> e = first;
   HashEntry<K,V> node = null;
   int retries = -1; // negative while locating node
   //尝试性加锁失败、
   //那么就对segment[hash]对应的链表进行遍历找到需要
   //put的这个entry所在的链表中的位置
     while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //尝试性获取锁失败初始retries = -1 进入下面的步骤
        if (retries < 0) {
            //判断e是不是null,是通过hash找到的这个节点,刚好是空,或者就是最后一个节点
            if (e == null) {
              if (node == null) //大胆的创建节点  如果node是null就new一个节点
                   node = new HashEntry<K,V>(hash, key, value, null);
                  retries = 0;//将retries 改为0
              }
               else if (key.equals(e.key))//遍历过程中发现我们要找的地方
                      retries = 0;//将其改为0
                    else
                       e = e.next;//不是我们要找的,否则找下一个节点
                }
                //前置++判断是否超过我们之前设置最大次数,
                //好像是64次,超过就将其锁住,进入阻塞等待
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; //有可能是别的线程将其更改,则重新遍历
                    retries = -1;
                  }
    }
     return node;
}

(图片均为网上下载 侵删)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值