并发容器-ConcurrentHashMap

早期

  Vector和Hashtable

    方法全是 同步方法

    早期JDK

    并发性能差

  ArrayList和HashMap

  1.     并发性能差
  2.     虽然这两个类不是线程安全的,但是可以用
  3.     Collections.synchronizedList(new ArrayList<E>))
  4.     Collections.synchronizedMap(new HashMap<K,V>0

HashMap 线程不安全原因

同时 put 碰撞导致数据丢失

        有多个线程同时使用 put 来添加元素,而且恰好两个 put 的 key 是一样的,它们发生了碰撞,也就是根据 hash 值计算出来的 bucket 位置一样,并且两个线程又同时判断该位置是空的,可以写入,所以这两个线程的两个不同的 value 便会添加到数组的同一个位置,这样最终就只会保留一个数据,丢失一个数据。

可见性问题无法保证

        如果某一个数据结构声称自己是线程安全的,那么它同样需要保证可见性,也就是说,当一个线程操作这个容器的时候,该操作需要对另外的线程都可见,也就是其他线程都能感知到本次操作。可是 HashMap 对此是做不到的,如果线程 1 给某个 key 放入了一个新值,那么线程 2 在获取对应的 key 的值的时候,它的可见性是无法保证的,也就是说线程 2 可能可以看到这一次的更改,但也有可能看不到。所以从可见性的角度出发,HashMap 同样是线程非安全的。

死循环造成的CPU100%,Jdk 1.7 之前,多线程的情况下,同时扩容,会导致循环链表

HashMap关于并发的特点

  1. 非线程安全
  2. 迭代时不允许修改内容
  3. 只读的并发是安全的
  4. 如果一定要把HashMap用在并发环境,用Collections.synchronizedMap(new HashMap())

源码分析

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    } 
​
    //modCount++ 是一个复合操作
    modCount++;
 
    addEntry(hash, key, value, i);
    return null;
}

扩容期间取出的值不准确

        HashMap 本身默认的容量不是很大,如果不停地往 map 中添加新的数据,它便会在合适的时机进行扩容。而在扩容期间,它会新建一个新的空数组,并且用旧的项填充到这个新的数组中去。那么,在这个填充的过程中,如果有线程获取值,很可能会取到 null 值,而不是我们所希望的、原来添加的值

public class HashMapNotSafe {
 
    public static void main(String[] args) {
        final Map<Integer, String> map = new HashMap<>();
 
        final Integer targetKey = 0b1111_1111_1111_1111; // 65 535
        final String targetValue = "v";
        map.put(targetKey, targetValue);
 
        new Thread(() -> {
            IntStream.range(0, targetKey).forEach(key -> map.put(key, "someValue"));
        }).start();
 
        while (true) {
            if (null == map.get(targetKey)) {
                throw new RuntimeException("HashMap is not thread safe.");
            }
        }
    }
}

Java 7 结构

  1.  ConcurrentHashMap 内部进行了 Segment 分段,Segment 继承了 ReentrantLock,可以理解为一把锁,各个 Segment 之间都是相互独立上锁的,互不影响。相比于之前的 Hashtable 每次 操作都需要把整个对象锁住而言,大大提高了并发效率。因为它的锁与锁之间是独立的,而不是整个对象只有一把锁
  2. 每个 Segment 的底层数据结构与 HashMap 类似 仍然是数组和链表组成的拉链法结构。默认有 0~15 共 16 个 Segment,所以最多可以同时支持 16 个线程并发操作(操作分别分布在不同的 Segment 上)。16 这个默认值可以在初始化的时候设置为其他值,但是一旦确认初始化以后,是不可以扩容的。

Java8结构

并发度

  1. Java 7 中,每个 Segment 独立加锁,最大并发个数就是 Segment 的个数,默认是 16。
  2. JDK 1.8 最大并发度是 Node 数组的大小,并发度更大

锁粒度更细

        synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。

保证并发安全的原理

  1.   Java 7 采用 Segment 分段锁来保证安全,而 Segment 是继承自 ReentrantLock。
  2.   Java 8 中放弃了 Segment 的设计,采用 Node volatile  + CAS + synchronized

Hash 碰撞

  1.   Java 7 在 Hash 冲突时,会使用拉链法,也就是链表的形式。
  2.   Java 8 先使用拉链法,在链表长度超过一定阈值时,将链表转换为红黑树,来提高查找效率。

查询时间复杂度

  1.   Java 7 遍历链表的时间复杂度是 O(n),n 为链表长度。
  2.   Java 8 如果变成遍历红黑树,那么时间复杂度降低为 O(log(n)),n 为树的节点个数。

Java 8 源码

Node 节点

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    // ...
}

put方法

会逐步根据当前槽点是未初始化、空、扩容、链表、红黑树等不同情况做出不同的处理。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) {
        throw new NullPointerException();
    }
    //计算 hash 值
    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();
        }
        // 找该 hash 值对应的数组下标
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //如果该位置是空的,就用 CAS 的方式放入新值
            if (casTabAt(tab, i, null,
                    new Node<K, V>(hash, key, value, null))) {
                break;
            }
        }
        //hash值等于 MOVED 代表在扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f);
        }
        //槽点上是有值的情况
        else {
            V oldVal = null;
            //用 synchronized 锁住当前槽点,保证并发安全
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //如果是链表的形式
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表
                        for (Node<K, V> e = f; ; ++binCount) {
                            K ek;
                            //如果发现该 key 已存在,就判断是否需要进行覆盖,然后返回
                            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;
      //到了链表的尾部也没有发现该 key,说明之前不存在,就把新值添加到链表的最后
                            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;
                        //调用 putTreeVal 方法往红黑树里增加数据
                        if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key,
                                value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent) {
                                p.val = value;
                            }
                        }
                    }
                }
            }
            if (binCount != 0) {
   //检查是否满足条件并把链表转换为红黑树的形式,默认的 TREEIFY_THRESHOLD 阈值是 8
                if (binCount >= TREEIFY_THRESHOLD) {
                    treeifyBin(tab, i);
                }
                //putVal 的返回是添加前的旧值,所以返回 oldVal
                if (oldVal != null) {
                    return oldVal;
                }
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

get 方法

  1. 计算 Hash 值,并由此值找到对应的槽点;
  2. 如果数组是空的或者该位置为 null,那么直接返回 null 就可以了;
  3. 如果该位置处的节点刚好就是我们需要的,直接返回该节点的值;
  4. 如果该位置节点是红黑树或者正在扩容,就用 find 方法继续查找;
  5. 否则那就是链表,就进行遍历链表查找。
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //计算 hash 值
    int h = spread(key.hashCode());
//如果整个数组是空的,或者当前槽点的数据是空的,说明 key 对应的 value 不存在,直接返回 null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
        //判断头结点是否就是我们需要的节点,如果是则直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
 //如果头结点 hash 值小于 03非阻塞队列.md,说明是红黑树或者正在扩容,就用对应的 find 方法来查找
        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;
}

ConcurrentHashMap也不是线程安全的

原子性问题

          使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映ConcurrentHashMap的中间状态因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。

使用组合操作

同时get Put 会发生线程不安全

public boolean replace(K key, V oldValue, V newValue)

public V putIfAbsent(K key, V value)

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

Java 自带的 Unsafe 实现的 CAS。它在虚拟机层面确保了写入数据的原子性,比加锁的效率高得多

NamedThreadFactory namedThreadFactory = new NamedThreadFactory("测试concurrentHashMap 的原子 cas 操作", false);

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10,
        TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
        namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy()
);

ConcurrentHashMap<String, LongAdder> concurrentHashMap = new ConcurrentHashMap<>(Maps.newHashMapWithExpectedSize(10));
Callable<String> callable=()->{
    String s = ThreadLocalRandom.current().nextInt(10) + "item";
    concurrentHashMap.computeIfAbsent(s,k->{
        LongAdder longAdder = new LongAdder();
        longAdder.increment();
        return  longAdder;
    });
    return "";
};

ArrayList<Future<String>> futures = new ArrayList<>();
for (int i = 0; i <10 ; i++) {
    Future<String> submit = threadPoolExecutor.submit(callable);
    futures.add(submit);
}
threadPoolExecutor.shutdown();
threadPoolExecutor.awaitTermination(20,TimeUnit.SECONDS);

Map<String, Long> collect = concurrentHashMap.entrySet().stream().collect((Collectors.toMap(
        k -> k.getKey(),
        k -> k.getValue().longValue()
)));

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值