ConcurrentHashMap笔记

本文深入探讨了容器Map中HashMap与ConcurrentHashMap的性能差异,着重解析了ConcurrentHashMap如何通过分段锁机制提升并发操作效率,以及其在数据结构设计上的关键特性。包括对数据插入、获取过程的详细分析,以及弱一致性问题的解决策略。

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

 前序

     在容器Map中,HashMap对数据(key-value,简称mapping)的操作效率快,但是不加锁,不安全;虽然Hashtable对数据的操作是加锁的,安全,但是对象级加锁的,整个对象都加锁了,比如,现在需要对Hashtable进行put操作,而同时已经对Hashtable有get操作,则put操作需要等get操作完后释放对象的锁才继续进行,这样效率上会有所降低。即需要解决效率,也需要考虑问题,java提供了java.util.concurrent.ConcurrentHashMap类对此类问题有很大的帮助。

 基本数据结构

        ConcurrentHashMap的锁不是针对ConcurrentHashMap整个对象级的,它的锁是分段的,如果操作的数据不存在同个段,则不会有被锁的困扰。下面是ConcurrentHashMap类简要图:

      ConcurrentHashMap对mapping是保存在数组Segment[],也可以说是分段保存,ConcurrentHashMap根据mapping的key的hash值生成段的下标index,则保存在segments[index]下,则key的hash值相等的mapping都保存在同个segments[index]下,这点跟HashMap是类似的。segments[index]即Segment对象,ConcurrentHashMap的锁是加在Segment对象上的,其它segments[index]是不受影响的,也可以说ConcurrentHashMap的锁是分段锁,这样,即使某一段被加锁了,其它段的操作是不受影响的,这样就加快了ConcurrentHashMap的效率。

      生成maping所在段(segments[index])的下标的方法:

    /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because ConcurrentHashMap uses power-of-two length hash tables,
     * that otherwise encounter collisions for hashCodes that do not
     * differ in lower or upper bits.
     */
    private static int hash(int h) {
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

      Segment中使用数组HashEntry[]存储mapping,Segment根据mapping的key的hash值生成保存的数组下标index,如果Segment生成的下标是相同,则用HashEntry的next属性链接,由于可看ConcurrentHashMap也使用了列表、链表来保存数据。

      Segment保存mapping在数据HashEntry[ ] 中的下标index,是这样生成的:

HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
      其中hash是key的哈希值,table是Segment中的数组HashEntry[] table。

以下是Segment和HashEntry:



由上面两个结构,可以看出Entry<K,V>的值V是volatile修饰的,Segment<K,V>的HashEntry<K,V>数组table也是volatile修饰的,这使得在一个线程下修改一个元素的值,或者数组table有变动(比如增加,删除元素),则其他所有线程都可以知道这个变动,从而达到数据变动的透明性。

ConcurrentHashMap中存储数据各个结构的关系如下:

下面从源码入手来看下元素是怎么插入ConcurrentHashMap的

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {

    final Segment<K,V>[] segments;

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SBASE;
    private static final int SSHIFT;
    private static final long TBASE;
    private static final int TSHIFT;
    private static final long HASHSEED_OFFSET;
    private static final long SEGSHIFT_OFFSET;
    private static final long SEGMASK_OFFSET;
    private static final long SEGMENTS_OFFSET;

    static {
        int ss, ts;
        try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class tc = HashEntry[].class;
        Class sc = Segment[].class;
        //UNSAFE.arrayBaseOffset(Class arrayClass):获取给定数组中第一个元素的偏移地址
        TBASE = UNSAFE.arrayBaseOffset(tc);
        SBASE = UNSAFE.arrayBaseOffset(sc);
        //UNSAFE.arrayIndexScale(Class arrayClass):获取比例因子,这个比例因子可以用来对指定的数组类的元素进行寻址
        ts = UNSAFE.arrayIndexScale(tc);
        ss = UNSAFE.arrayIndexScale(sc);
        HASHSEED_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("hashSeed"));
        SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentShift"));
        SEGMASK_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentMask"));
        //UNSAFE.objectFieldOffset(Field f):返回指定静态属性在内存地址中偏移量,这个偏移量仅仅是用作访问这个类的指定属性的一种方式。
        SEGMENTS_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segments"));
    } catch (Exception e) {
        throw new Error(e);
    }

    if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)
                throw new Error("data type scale not a power of two");
    //Integer.numberOfLeadingZeros(int i)的解释说明:如下
    //1024以2为底的对数:10
    //10 = 31 - Integer.numberOfLeadingZeros(1024)
    SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
    TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
    }

    //插入元素
    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        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的put方法插入元素
       return s.put(key, hash, value, false);
    }

    //获取元素在ConcurrentHashMap中对应段(即Segment<K,V>[]数组中的Segment<K,V>元素)
    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;
        //UNSAFE.getObjectVolatile(Object obj, long offset):返回对象obj中指定地址偏移量的属性,支持volatile load语义
        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) {
                    //以CAS的原子操作方式将数组Segment<K,V>[] ss中偏移量为u的位置更新为Segment<K,V> s
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
    //..其他省略    
}

下面看看Segment是如何插入元素的,其中我们来看看put方法。

static final class Segment<K,V> extends ReentrantLock{
    //..其他省略

    final V put(K key, int hash, V value, boolean onlyIfAbsent){
        //获得锁
        tryLock(); //tryLock() ==>> ReentrantLock.tryLock() ==>>sync.nonfairTryAcquire(1)
        try{
            //put 操作。。省略。。
        }finally{
            //释放锁sync.release(1),传值1,因为lock和unlock是一一对应的,获得锁一次,就要释放一次。
            unlock(); //unlock ==>> sync.release(1);
        }
    }
}
ReentrantLock的相关代码

public class ReentrantLock implements Lock{
	Sync sync;
	abstract static class Sync extends AbstractQueuedSynchronizer{

		//获得锁
		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //state值为0,表示没有线程获得当前锁
            if (c == 0) {
            	//进行CAS(比较并交换)设置,CAS是CPU的指令,硬件的CPU会保证CAS指令是原子指令操作
                if (compareAndSetState(0, acquires)) {
                	//compareAndSetState方法返回true,表示设置成功,
                	//当前线程可以获得锁,并设置锁的持有人是当前线程:setExclusiveOwnerThread(current)
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//表示锁的持有人是当前线程,即线程再次获得了锁,这是没问题的
                int nextc = c + acquires;//因为acquires传进来的值是1,表示当前线程又一次获得该锁,nextc表示线程重复获得锁次数
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        //释放锁
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//c为0,表示线程对锁已经全部释放,则锁是自由了free。(如果线程多次获得一个锁,则需要多次释放锁)
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
	}
	class NonfairSync extends Sync{..}
	class FairSync extends Sync{..}
	ReetrantLock(){
		sync = new NonfairSync();
	}
	//...其他省略
}

AbstractQueuedSynchronizer的相关代码

public abstract class AbstractQueuedSynchronizer{
    /**
     * The synchronization state.
     *
     */
    //volation可以使得一旦state被修改,其他所有线程都可以知道state的最新值(即处理器的缓存区的缓存被设置无效,state需要重新从内存获取)
    //初始化值为0,则表示锁没有被获得。
    private volatile int state;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long stateOffset;

    static {
        try {
            //获取state的偏移量,以便使用CAS原子操作
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));

        } catch (Exception ex) { throw new Error(ex); }
    }

    //CAS操作,修改state的值
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
}

想悉CAS(比较并交换)原理,可以查看点击打开链接

ConcurrentHashMap的弱一致性


JDK1.6源码


我们主要讨论get方法的弱一致性,即是说,ConcurrentHashMap的底层数据结构已经加入一个map<k,v>元素后,在get方法里有可能不能马上取到对应元素的值。
底层数据结构主要是Segment中的HashEntry<K,V>[] table保存了map<k,v>元素,也就是table已经保存了元素,在调用get方法时,有可能不能立刻取到对应元素。
下面结合源码和JMM来看看。

主要看java.util.concurrent.ConcurrentHashMap.Segment<K, V>的put,get方法
Segment的结构主要有: HashEntry<K,V>[] table,count是volatile的;
HashEntry的结构主要看: value是volatile的,则会有线程可见性。

V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();
    try {
        int c = count;
        if (c++ > threshold) // ensure capacity
            rehash();
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;

        V oldValue;
        if (e != null) { //1处,key已存在的情况
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value; //2处,只需赋值value的值即可
        }
        else {
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K,V>(key, hash, first, value); //3处,创建一个新的HashEntry,然后再赋值给tab[index]
            count = c; // write-volatile  //4处,在3处赋值完tabl[index]后,再修改count的值,count是volatile的,对其他线程是可见性的。
        }
        return oldValue;
    } finally {
        unlock();
    }
}

V get(Object key, int hash) {
    if (count != 0) { // read-volatile  //5处,需要在count值不为0才会获取值value
        HashEntry<K,V> e = getFirst(hash); //6处
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value; //8处
                if (v != null)
                    return v;
                return readValueUnderLock(e); // recheck //7处
            }
            e = e.next;
        }
    }
    return null;
}

HashEntry<K,V> getFirst(int hash) {
    HashEntry<K,V>[] tab = table;
    return tab[hash & (tab.length - 1)];
}
其中put方法从开始有加锁,到方法结束释放锁;而get方法开始是没有加锁的。
put方法分两个情况:
1.加入的元素<k,v>原先是已经存在的
即是在"1处"的,如果key是存在的,则对应"2处",只要修改value的值,由于value是volatile的即是对其他线程可见的,只要修改后,则是可以get得到的,不用等待put方法的结束,不会有弱一致性的问题。
2.加入的元素<k,v>原先是不存在的
即对应到"3处",需要创建一个新的HashEntry,然后再赋值给tab[index],在"3处"操作完之后 ,再"4处"操作,修改count的值,count是volatile的,对其他线程是可见性的。但对于get方法来说,需要判断count值不为0才会获取值value,即是"5处",如果刚好是"3处"操作完,底层数据结构已经保存了加入的元素,但是"4处"还没修改count值之前,就执行了"5处"操作,则会产生弱一致性的问题,get不到加入元素的value。

看到“7处”,当e.value为null时,会调用readValueUnderLock方法获取value值。
/**
 * Reads value field of an entry under lock. Called if value
 * field ever appears to be null. This is possible only if a
 * compiler happens to reorder a HashEntry initialization with
 * its table assignment, which is legal under memory model
 * but is not known to ever occur.
 */
V readValueUnderLock(HashEntry<K,V> e) {
    lock();
    try {
        return e.value;
    } finally {
        unlock();
    }
}
对于“8处”,e.value,e是HashEntry是table的元素,虽然table数组是volatile的,但table的元素HashEntry并不是volatile的,在旧的JMM里,通过非volatile变量访问volatile变量是可以被编译器重排的(不过在新的JMM会加强volatile语义,几乎不会有这种情况),当编译器重排了HashEntry的初始化时,value域的值可能为null,会调用readValueUnderLock,但这种情况是很少出现的,所以这个方法只是个备用方案,这种重排对于旧的JMM(JSR 133之前)来说是合法的。
readValueUnderLock这里会加锁来使用同步获取value的值,根据JMM的Synchronization Order,unlock前的操作结果肯定对lock后的操作可见,即是unlock happen before lock,所以通过加锁能获取到之前value的赋值。
继续看源码对HashEntry的解释:
/**
 * ConcurrentHashMap list entry. Note that this is never exported
 * out as a user-visible Map.Entry.
 *
 * Because the value field is volatile, not final, it is legal wrt
 * the Java Memory Model for an unsynchronized reader to see null
 * instead of initial value when read via a data race.  Although a
 * reordering leading to this is not likely to ever actually
 * occur, the Segment.readValueUnderLock method is used as a
 * backup in case a null (pre-initialized) value is ever seen in
 * an unsynchronized access method.
 */
static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;

    HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
        this.key = key;
        this.hash = hash;
        this.next = next;
        this.value = value;
    }
}
value域是volatile的,而不是final,在旧的JMM里,由于编译器可能对程序的重排,会导致非同步模式下的程序在有线程竞争之下可能看到null(即是HashEntry初始化之前的值)而不是初始值。
Java的final和volatile详细说明,可参照:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalRight

JDK1.7源码

主要看java.util.concurrent.ConcurrentHashMap.Segment<K, V>的put,get方法
Segment的HashEntry<K,V>[] table、HashEntry的value是volatile,Segment的count不是volatile的。

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;  //61处
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first); //6处,创建新的HashEntry
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node); //7处,将新的HashEntry设置到HashEntry[] table中
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i, HashEntry<K,V> e) {
    UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}

static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
    return (tab == null) ? null : (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)i << TSHIFT) + TBASE);
}

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) { //8处
        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;
}
如果加入的元素是已经存在的,则也是修改HashEntry的value值就可以了,value就volatile的,不存在弱一致性问题;
如果加入的元素是不存在的,也不会存在弱一致性问题了,因为1.7源码中已经不再用count来作为判断条件来get元素的value了,而是使用UNSAFE.getObjectVolatile()方法可以直接拿到新加入的元素,即是"8处",volatile是可见性的,所以不存在弱一致性问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值