ConcurrentHashMap & Hashtable

ConcurrentHashMap 和 Hashtable 的区别

主要提醒在实现线程安全的方式上不同

  • 底层数据结构
    • JDK1.7 的ConcurrentHashMap 底层采用 分段数组 + 链表 实现, JDK 1.8 采用数据结构与HashMap 1.8 的结构一样, 数组 + 链表/红黑树, Hashtable 底层数据结构采用 数组 + 链表 形式
  • 实现线程安全的方式
    • ConcurrentHashMap
      • 在JDK 1.7 时, ConcurrentHashMap (分段锁)对整个桶数组进行了分割分段(segment), 每把锁只锁容器其中一部分数据, 多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率.
      • 在JDK 1.8 时, 直接用 Node 数组 + 链表 + 红黑树的数据结构来实现, 并发控制使用 synchronized 和CAS 来操作, 整体看起来就像是优化过且线程安全的HashMap
    • Hashtable
      • 使用 synchronized 来保证线程安全,效率非常低,当一个线程访问同步方法时, 其他线程也访问同步方法,可能会阻塞或轮询整体.当put元素,另一个线程不能使用put方法添加元素,也不能使用get. 竞争会越来越激烈,效率越低

加锁方式

Hashtable

HashTable加锁方式

ConcurrentHashMap

JDK1.7 ConcurrentHashMap

加锁原理

首先将数据分宜一段一段的存储, 然后给每一段数据分配一把锁, 当一个线程占用锁访问其中一段数据时, 其他段的数据也能被其他线程访问.
ConcurrentHashMap 是由 Segment 数据结构 + HashEntry 数据结构组成
Segment 实现了 ReentrantLock, 是一种可重入锁, 扮演锁的角色, HashEntry 用于存储数据
一个ConcurrentHashMap中包含一个 Segment 数组, Segment中又包含一个HashEntry数组,当对HashEntry数组中的数据进行修改是,必须获取对应的Segment 锁

Segment 是 ConcurrentHashMap 的一个内部类

static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        
    transient int modCount;
        // 大小
    transient int threshold;
        // 负载因子
    final float loadFactor;

}

使用了 volatile 修饰

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

每当一个线程占用锁访问一个 Segment 时, 不会影响到其他的 Segment
如果容量大小是16,那么他的并发度就是16, 可以同时允许16个线程操作16个 Segment 且线程安全

put() 方法逻辑

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();//这就是为啥他不可以put null值的原因
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          
         (segments, (j << SSHIFT) + SBASE)) == null) 
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 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;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
 // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                        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 {
                 // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                        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;
        }

首先第一波会尝试获取锁, 如果获取失败就说明有其他线程存在竞争, 利用 scanAndLockForPut() 自旋获取锁, 如果充实的次数到达了MAX_SCAN_RETRIES则改为阻塞锁获取,保证能获取成功

get()方法逻辑

获取元素只需要将 key 通过 hash之后定位到具体的 Segment, 在通过一次hash 定位到具体的元素
由于 HashEntry 中 value 属性使用 volatile 修饰, 保证内存的可见性, 所以每次获取都是最新值, 获取过程不需要加锁

存在的问题

因为基本上都是数组 + 链表 形式, 查询的时候需要遍历链表, 会导致效率很低, 与 JDK 1.7 的HashMap 一样, 在 JDK 1.8 中优化

JDK1.7 ConcurrentHashMap 加锁方式

JDK1.8 ConcurrentHashMap

加锁原理

取消了Segment 分段锁, 采用CAS + Synchronized 来保证并发安全, 数据结构与 HashMaori 1.8 结构类似, 数组 + 链表 / 红黑树. 在链表长度超过阈值8是将链表转换为红黑树. 把之前的 HashEntry 改成 Node, 作用不变, 把值和 next 采用了 volatile 修饰, 保证可见性
Synchronized 只锁定当前链表或红黑树的首节点, 这样只要hash不冲突, 就不会产生并发,效率又提升

put() 方法逻辑

  1. 根据 key 计算出 hashcode
  2. 判断是否需要初始化
  3. 即为当前 key 定位出的 Node, 如果为空表示当前位置可以写入数据, 利用 CAS 尝试写入, 失败则自旋保证成功
  4. 如果当前位置的 hashcode == MOVED == -1 , 则需要进行火绒
  5. 如果都不满足, 则利用 synchronized 锁写入数据
  6. 如果数量大于 TREEIFY_THRESHOLD 则转化为红黑树
	final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //根据 key 计算出 hashcode
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //判断是否需要进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	//为当前 key 定位出的 Node, 如果为空表示当前位置可以写入数据, 利用 CAS 尝试写入, 失败则自旋保证成功
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //如果当前位置的 hashcode == MOVED == -1 则需要进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
            	//如果都不满足, 利用 synchronized 锁写入数据
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                	//如果数量大于 TREEIFY_THRESHOLD 则转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

get() 方法逻辑

  • 根据计算出来的 hashcode 值确定节点位置
  • 如果是搜索到的节点 key 与传入的 key 相同且不为null, 直接返回这个节点
  • 如果是红黑树则按照树的方法获取值
  • 都不满足则按照链表方法遍历获取值
	public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        //根据计算出来的 hashcode 值确定节点位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果是搜索到的节点 key 与传入的 key 相同且不为null, 直接返回这个节点
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //说明该节点在树上, 根据树方法寻址
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //都不满足则按照链表放肆遍历获取值
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

JDK1.8 ConcurrentHashMap 加锁方式

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值