深入解析Java ConcurrentHashMap类的源码

前言

  ConcurrentHashMap是Java中的高并发容器,具有线程安全、高并发、高性能等特点。在本文中,我们将通过分析ConcurrentHashMap的数据结构、锁机制、LongAdder和computeIfAbsent方法的实现原理,来深入了解这一容器类。

ConcurrentHashMap的数据结构

  ConcurrentHashMap是基于哈希表实现的,其底层数据结构是由多个Segment组成的Segment数组,每个Segment内部都是一个与HashMap类似的链表结构(JDK1.7),或者是一个数组加链表结构(JDK1.8)。对于一个键值对,首先会根据其键值的哈希值定位到一个Segment,然后在该Segment内部进行操作。

  以下是ConcurrentHashMap的构造方法源码:

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0F) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // java8之前
        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;
        // Segments数组中存放Segment对象
        Segment<K,V>[] segments = (Segment<K,V>[])new Segment[cap];
        // 构造每个Segment对象
        for (int i = 0; i < segments.length; ++i)
            segments[i] = new Segment<K,V>(loadFactor);
        this.segments = segments;
    }

  在源码中,我们可以看到ConcurrentHashMap的构造方法接收三个参数:initialCapacity表示初始容量大小,loadFactor表示负载因子,concurrencyLevel表示并发级别。其中,concurrencyLevel参数用来初始化ConcurrentHashMap的Segment数组大小,也就是ConcurrentHashMap内部开启的线程数量,因为每个Segment都会对应一个独立的线程。在初始化过程中,通过Segment数组进行ConcurrentHashMap的并发控制。

ConcurrentHashMap的锁机制

  在ConcurrentHashMap中,对于每个Segment,使用重入锁(ReentrantLock)来实现线程安全。对于已经锁定的Segment,在插入或删除元素时,只有等待其它线程操作完成后才能进行,以此避免多个线程同时操作同一个Segment导致的并发问题。而对于未被锁定的Segment,可以并发地执行读取操作,不会出现并发问题。

  以下是ConcurrentHashMap的Segment的构造方法和插入元素的方法源码:

    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private transient volatile int count;
        transient int modCount;
        transient int threshold;
        transient volatile HashEntry<K,V>[] table;
        
        final float loadFactor;

        Segment(float lf) {
            this.loadFactor = lf;
            this.threshold = (int)(DEFAULT_CAPACITY * lf);
            this.table = new HashEntry[DEFAULT_CAPACITY];
        }
        
        V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 插入元素时先获取锁
          lock();
          // ...
        }
        
        // ...
    }

  在上述代码中,我们可以看到,在Segment的构造方法中通过继承ReentrantLock实现了对Segment的重入锁机制。而在插入元素时,则需要首先获取Segment的锁,保证只有一个线程在进行插入操作。

ConcurrentHashMap的并发度与性能

  ConcurrentHashMap的并发度和性能表现非常优秀,主要得益于其对读写操作的优化。由于ConcurrentHashMap在初始化时会指定内部开启的线程数量,因此可以避免像同步容器那样在进行读取、插入或删除操作时,多个线程被阻塞的问题,大大提升了并发性能。

  另外,ConcurrentHashMap中的元素插入和查询操作都使用了volatile关键字,保证对元素的修改和读取操作可见性,进一步提高了并发性能。

LongAdder的实现原理

  LongAdder是ConcurrentHashMap中的一个类,用于对值进行累加计数。与AtomicLong不同,LongAdder分散了计数,因此在高并发环境下比AtomicLong要快。具体来说,LongAdder使用了分段锁机制,将计数值分别记录在各自的cell中,每次更新时只需要对对应的cell进行加法计算,而不需要对所有的计数值进行加法计算。

  以下是LongAdder的源码:

    public class LongAdder {
        private static final Unsafe UNSAFE = Unsafe.getUnsafe();
        private static final long BASE;
        private static final int SHIFT;
        private static final int CELLSBUSY;
        private static final long[] PROBE;
        volatile transient Cell[] cells;
        public LongAdder() {
            this.cells = null;
        }

        public void add(long x) {
            Cell[] as;
            long b, v;
            int m;
            Cell a;
            if ((as = this.cells) != null || !this.casBase(b = this.base, b + x)) {
                boolean uncontended = true;
                if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) {
                    this.longAccumulate(x, (LongBinaryOperator)null, uncontended);
                }
            }

        }

        final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
            int h;
            if ((h = getProbe()) == 0) {
                ThreadLocalRandom.current();
                h = getProbe();
                this.setProbe(h == 0 ? 1 : h);
                wasUncontended = true;
            }

            boolean collide = false;

            while(true) {
                Cell[] as;
                Cell a;
                int n;
                if ((as = this.cells) != null && (n = as.length) > 0) {
                    if ((a = as[h & n - 1]) == null) {
                        if (this.busy == 0) {
                            Cell r = new Cell(x);
                            if (this.busy == 0 && this.casBusy()) {
                                boolean created = false;

                                try {
                                    Cell[] rs;
                                    int m;
                                    int j;
                                    if ((rs = this.cells) != null && (m = rs.length) > 0 && rs[j = n - 1 & h] == null) {
                                        rs[j] = r;
                                        created = true;
                                    }
                                } finally {
                                    this.busy = 0;
                                }

                                if (created) {
                                    break;
                                }

                                continue;
                            }
                        }

                        collide = false;
                    } else if (!wasUncontended) {
                        wasUncontended = true;
                    } else {
                        long v = a.value;
                        if (a.cas(v, fn == null ? v + x : fn.applyAsLong(v, x))) {
                            break;
                        }

                        if (n >= CELLSBUSY || this.cells != as) {
                            collide = false;
                        } else if (!collide) {
                            collide = true;
                        } else if (this.busy == 0 && this.casBusy()) {
                            try {
                                if (this.cells == as) {
                                    Cell[] rs = new Cell[n << 1];

                                    for(int i = 0; i < n; ++i) {
                                        rs[i] = as[i];
                                    }

                                    this.cells = rs;
                                }
                            } finally {
                                this.busy = 0;
                            }

                            collide = false;
                            continue;
                        }
                    }

                    h ^= h << 13;
                    h ^= h >>> 17;
                    h ^= h << 5;
                    this.setProbe(h);
                    continue;
                }

                if (this.busy == 0 && this.cells == as && this.casBusy()) {
                    boolean init = false;

                    try {
                        if (this.cells == as) {
                            Cell[] rs = new Cell[2];
                            rs[h & 1] = new Cell(x);
                            this.cells = rs;
                            init = true;
                        }
                    } finally {
                        this.busy = 0;
                    }

                    if (init) {
                        break;
                    }
                    continue;
                }

                if (this.casBase(b, fn == null ? b + x : fn.applyAsLong(b, x))) {
                    break;
                }
            }

        }
        
        // ...
    }

  上述代码中,我们可以看到LongAdder实现的核心是通过Cell数组来记录各自的计数值,并使用分段锁机制实现并发控制,从而提高了并发性能。

computeIfAbsent方法的实现原理

  computeIfAbsent是ConcurrentHashMap中的一个方法,用于在ConcurrentHashMap中的键key不存在时,通过计算方式新增一个键值对。首先,computeIfAbsent方法会根据key的hash值得到其在ConcurrentHashMap中对应的Segment。然后,在该Segment内部进行操作:首先会对Segment进行加锁,然后判断该key是否存在,如果不存在,则通过Function计算出对应的value值,并插入到该Segment中。

  以下是computeIfAbsent方法的源码:

    public V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        if (key == null || mappingFunction == null)
            throw new NullPointerException();
        // 计算key的哈希值
        int h = spread(key.hashCode());
        // 插入元素时先获取锁
        Segment<K,V> segment = segmentForHash(h);
        return segment.compute(key, h, mappingFunction, false);
    }

    public V compute(K key,
                    BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (key == null || remappingFunction == null)
            throw new NullPointerException();
        int h = spread(key.hashCode());
        Segment<K,V> segment = segmentForHash(h);
        return segment.compute(key, h,
                               (k, oldValue) -> {
                                   V newValue = remappingFunction.apply(k, oldValue);
                                   return (newValue == null) ?
                                          null :
                                          checkType(newValue);
                               },
                               true);
    }

  从上述代码中,我们可以看到,在Segment的compute方法中,如果key不存在于Segment中,则调用mappingFunction计算出对应的value,并插入到Segment中。compute方法使用了CAS无锁技术实现了线程安全。

总结

  ConcurrentHashMap是一个高并发容器,其源码实现比较复杂,本文从数据结构、锁机制、并发度和性能、LongAdder和computeIfAbsent等方面进行分析。无论是在学习Java并发编程,还是在实际应用中操作高并发容器,对于ConcurrentHashMap的深入了解都有重要的意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值