JAVA多线程之——ConcurrentHashMap

本文详细介绍了ConcurrentHashMap,它是HashMap的线程安全版本,通过使用Segment缩小锁的粒度来提高并发性能。相较于HashTable,ConcurrentHashMap在增加、删除、修改和获取元素时具有更高的效率。文章探讨了其内部结构、优点和操作过程,并对比了与HashTable的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ConcurrentHashMap

学习了ArrayList与HashSet相对应的线程安全的CopyOnWriteArrayList 与CopyOnWriteArraySet之后,今天学习HashMpa对应的线程安全集合ConcurrentHashMap.
HashMap是线程不安全的,HashTable是线程安全的。但是HashTable是通过Synchronized进行同步的,这就导致一个线程占有锁,其它不管读写线程都需要等待,效率地下。
ConcurrentHashMap就解决这个问题。HashTable是把整个链表进行加锁,ConcurrentHashMap就是通过缩小锁的粒度来解决。
ConcurrentHashMap结构
这里写图片描述
从结构图中,对比,HashTable中维护的是一个链表,而在ConCurrentHashMap中,不是直接维护一个Hash链表,而是在中间加入了一个Segment(段)的概念。每个Segment去维护一个Hash链表。ConcurrentHashMap就是靠这个段来缩小锁的粒度。也就是说,ConcurrentHashMap把真个链表拆成多个Segment,由他们来各自维护一个链表,这样当要进行操作时,只需要操作对应的Segment就可以,而不需要像HashTable一样操作的是一个整体的链表。
优点
缩小的锁的粒度,这样大大增加了并发的能力
缺点
定位到具体的对象,需要进过两次Hash运算。第一次定位到段,再定位到链表头部。这样hash的过程要比HashTable长。
Segment

static final class Segment<K,V> extends ReentrantLock implements Serializable {
 transient volatile int count; //Segment中元素的数量
  transient int modCount;  //对table的大小造成影响的操作的数量
  transient int threshold;//Segment扩容的阀值
  transient volatile HashEntry<K,V>[] table;//链表数组,数组中的每一个元素代表了一个链表的头部
  final float loadFactor;//负载因子
}

HashEntry

 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;
        }
    }

hash和key都被定义为final类型,这样可以保证链表的机构不会出现被破坏的现象。
了解了段和链表的机构,就可以来看看ConcurrentHashMap是如何添加、删除、修改、获取的了。
get

 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;
    }

看上去挺复杂,其实就是第一步,通过hash key得到key的hash值,然后根据这个值与segmentShift和segmentMask进行运算,定位到对象在哪个段上面。
第二步,循环这个链表获取对应key的值。
put

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    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;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
  1. 获取锁
  2. 定位到具体的HashEntry
  3. 遍历HashEntry链表,如果key已存在 再判断传入的onlyIfAbsent的值 ,再决定是否覆盖旧值.
  4. 释放锁,返回旧值.

remove

      final V remove(Object key, int hash, Object value) {
            if (!tryLock())
                scanAndLock(key, hash);
            V oldValue = null;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> e = entryAt(tab, index);
                HashEntry<K,V> pred = null;
                while (e != null) {
                    K k;
                    HashEntry<K,V> next = e.next;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        V v = e.value;
                        if (value == null || value == v || value.equals(v)) {
                            if (pred == null)
                                setEntryAt(tab, index, next);
                            else
                                pred.setNext(next);
                            ++modCount;
                            --count;
                            oldValue = v;
                        }
                        break;
                    }
                    pred = e;
                    e = next;
                }
            } finally {
                unlock();
            }
            return oldValue;
        }

先获取锁,然后定位到HashEntry,然后删除该元素,修改链表的节点链接。再以前的版本中的删除是,会把被删除元素的前面的元素都复制一遍,然后在删除之后在把复制的添加到链表中。因为此前版本中

 volatile HashEntry<K,V> next;

HashEntry中的下一个节点都是被定义为final类型的。
size

 public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
             if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }          
               sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

获取size其实可以理解为对每个段中的链表节点数量进行相加就行,但是由于存在并发问题,所以事先要获取每一个段的锁。然后进行相加。

 if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }      

这段代码就是获取段的锁。
注意jdk1.7与1.6的实现是不一样的具体详情可以参考:http://www.jianshu.com/p/bd972088a494

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值