美文网首页
ConcurrentHashMap解析

ConcurrentHashMap解析

作者: jxiang112 | 来源:发表于2018-12-29 15:10 被阅读16次

注:此文基于Android的ConcurrentHashMap
讲解ConcurrentHashMap之前,我先花点篇幅讲解下CAS的机制思想,因为ConcurrentHashMap的核心使用CAS。
CAS: (全称叫Compare And Swap)翻译过来是比较并交换。什么意思呢?其实它的思想就是在写时,先比较内存值(V)和旧的预期值(A)是否相等,如果相等则修改内存值为新值(B)并返回true;如果不相等则什么也不做返回false。CAS是可以确保原子操作的。
java中CAS通过UnSafe类实现,UnSafe类提供了硬件级别的原子操作,我们看下UnSafe源码:

public final class Unsafe {
    //UnSafe单例实例
    private static final Unsafe theUnsafe;
    //无效字段的偏移地址
    public static final int INVALID_FIELD_OFFSET = -1;
    /**
     ARRAY_XX_BASE_OFFSET代表某一类型的数组第一个元素的偏移地址
     ARRAY_XX_INDEX_SCALE 代表某一类型的数组中元素的增量地址,比如int此值为4
    */
    public static final int ARRAY_BOOLEAN_BASE_OFFSET;
    public static final int ARRAY_BYTE_BASE_OFFSET;
    public static final int ARRAY_SHORT_BASE_OFFSET;
    public static final int ARRAY_CHAR_BASE_OFFSET;
    public static final int ARRAY_INT_BASE_OFFSET;
    public static final int ARRAY_LONG_BASE_OFFSET;
    public static final int ARRAY_FLOAT_BASE_OFFSET;
    public static final int ARRAY_DOUBLE_BASE_OFFSET;
    public static final int ARRAY_OBJECT_BASE_OFFSET;
    public static final int ARRAY_BOOLEAN_INDEX_SCALE;
    public static final int ARRAY_BYTE_INDEX_SCALE;
    public static final int ARRAY_SHORT_INDEX_SCALE;
    public static final int ARRAY_CHAR_INDEX_SCALE;
    public static final int ARRAY_INT_INDEX_SCALE;
    public static final int ARRAY_LONG_INDEX_SCALE;
    public static final int ARRAY_FLOAT_INDEX_SCALE;
    public static final int ARRAY_DOUBLE_INDEX_SCALE;
    public static final int ARRAY_OBJECT_INDEX_SCALE;
    public static final int ADDRESS_SIZE;
    
    /**
        注册本地方法
    */
    private static native void registerNatives();

    private Unsafe() {
    }
    
    //获取UnSafe单例实例对象
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    
    /**
    获取var1对象中偏移地址为var2的int类型的字段值
    var1:包含需要读取字段值的对象
    var2:  var1中int类型字段的偏移地址
    */
    public native int getInt(Object var1, long var2);
    /**
     设置var1对象中偏移地址为var2的int类型的字段值为var4
    */
    public native void putInt(Object var1, long var2, int var4);
    /**
    获取var1对象中偏移地址为var2、类型为Object的字段值
    */
    public native Object getObject(Object var1, long var2);
    /**
      设置var1对象中偏移地址为var2、类型为Object的字段值为var4
    */
    public native void putObject(Object var1, long var2, Object var4);
    /**
    获取var1对象中偏移地址为var2、类型为boolean的字段值
    */
    public native boolean getBoolean(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为boolean的字段值为var4
    */
    public native void putBoolean(Object var1, long var2, boolean var4);
    /**
    获取var1对象中偏移地址为var2、类型为byte的字段值
    */
    public native byte getByte(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为byte的字段值为var4
    */
    public native void putByte(Object var1, long var2, byte var4);
    /**
    获取var1对象中偏移地址为var2、类型为short的字段值
    */
    public native short getShort(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为short的字段值为var4
    */
    public native void putShort(Object var1, long var2, short var4);
    /**
    获取var1对象中偏移地址为var2、类型为char的字段值
    */
    public native char getChar(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为char的字段值为var4
    */
    public native void putChar(Object var1, long var2, char var4);
    /**
    获取var1对象中偏移地址为var2、类型为long的字段值
    */
    public native long getLong(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为long的字段值为var4
    */
    public native void putLong(Object var1, long var2, long var4);
    /**
    获取var1对象中偏移地址为var2、类型为float的字段值
    */
    public native float getFloat(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为float的字段值为var4
    */
    public native void putFloat(Object var1, long var2, float var4);
    /**
    获取var1对象中偏移地址为var2、类型为double的字段值
    */
    public native double getDouble(Object var1, long var2);
    /**
    设置var1对象中偏移地址为var2、类型为double的字段值为var4
    */
    public native void putDouble(Object var1, long var2, double var4);

     
    /**
    获取偏移地址为var1、类型为byte的字段值
    */
    public native byte getByte(long var1);
    /**
    设置偏移地址为var1、类型为byte的字段值为var3
    */
    public native void putByte(long var1, byte var3);
    /**
    获取偏移地址为var1、类型为short的字段值
    */
    public native short getShort(long var1);
    /**
    设置偏移地址为var1、类型为short的字段值为var3
    */
    public native void putShort(long var1, short var3);
    /**
    获取偏移地址为var1、类型为char的字段值
    */
    public native char getChar(long var1);
    /**
    设置偏移地址为var1、类型为char的字段值为var3
    */
    public native void putChar(long var1, char var3);
    /**
    获取偏移地址为var1、类型为int的字段值
    */
    public native int getInt(long var1);
    /**
    设置偏移地址为var1、类型为int的字段值为var3
    */
    public native void putInt(long var1, int var3);
    /**
    获取偏移地址为var1、类型为long的字段值
    */
    public native long getLong(long var1);
    /**
    设置偏移地址为var1、类型为long的字段值为var3
    */
    public native void putLong(long var1, long var3);
    /**
    获取偏移地址为var1、类型为float的字段值
    */
    public native float getFloat(long var1);
    /**
    设置偏移地址为var1、类型为float的字段值为var3
    */
    public native void putFloat(long var1, float var3);
    /**
    获取偏移地址为var1、类型为double的字段值
    */
    public native double getDouble(long var1);
    /**
    设置偏移地址为var1、类型为double的字段值为var3
    */
    public native void putDouble(long var1, double var3);
    /**
    获取偏移地址为var1、类型为long的字段值
    */
    public native long getAddress(long var1);
    /**
    获取偏移地址为var1、类型为long的字段值
    */
    public native void putAddress(long var1, long var3);
    /**
    请求分配内存var1大的内存,返回分配的起始地址
    */
    public native long allocateMemory(long var1);
    /**
    对偏移地址为var1的内存扩展var3的大小 
    */
    public native long reallocateMemory(long var1, long var3);
    /**
    
    */
    public native void setMemory(Object var1, long var2, long var4, byte var6);

    public void setMemory(long var1, long var3, byte var5) {
        this.setMemory((Object)null, var1, var3, var5);
    }

    public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

    public void copyMemory(long var1, long var3, long var5) {
        this.copyMemory((Object)null, var1, (Object)null, var3, var5);
    }
    /**
    释放内存
    */
    public native void freeMemory(long var1);
    /**
    获取静态域中给定字段var1的内存偏移地址
    */
    public native long staticFieldOffset(Field var1);
    /**
    返回对象中给定静态字段的内存地址的偏移量,在这个类的其他方法中这个值只是被用作访问特定字段的方式。这个值对给定的字段是唯一的,并且后续对该方法的调用返回值都是一样的
    */
    public native long objectFieldOffset(Field var1);
    /**
    返货静态域的起始内存地址
    */
    public native Object staticFieldBase(Field var1);
    /**
    判断给定的类var1是否已经初始化,如果已经初始化则放回true,否则返回false
    */
    public native boolean shouldBeInitialized(Class<?> var1);
    /**
    确定给定的类var1初始化,如果没有初始化则进行初始化,否则不做任何处理
    */
    public native void ensureClassInitialized(Class<?> var1);
    /**
    返回给定类型为var1数组的起始地址
    */
    public native int arrayBaseOffset(Class<?> var1);
    /**
    返回给定类型为var1数组的内存地址增量
    */
    public native int arrayIndexScale(Class<?> var1);
    /**
    返回内存地址大小?不知道什么用
    */
    public native int addressSize();
    /**
    返回内存分页大小,不知道什么用
    */
    public native int pageSize();
    
    public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

    public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
    
    public native Object allocateInstance(Class<?> var1) throws InstantiationException;

    public native void throwException(Throwable var1);
    
    /**
    CAS操作,如果对象var1中编译地址为var2的对象(对应的是内存值)与预期值的对象var4相等,则更改内存值为对象var5,并返回true;封装什么也不做并返回false
    */
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    /**
    CAS操作,如果对象var1中编译地址为var2的int值(对应的是内存值)与预期值的int值var4相等,则更改内存值为int值var5,并返回true;封装什么也不做并返回false
    */
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
    /**
    获取对象var1中偏移地址为var2、类型为Object的字段值。此操作可以保证返回的值是内存中最新的值——可见性,即Volatile
    */
    public native Object getObjectVolatile(Object var1, long var2);
    /**
    设置对象var1中偏移地址为var2、类型为Object的字段值为var4。此操作可以保证设置内存值之后值可见性,即Volatile
    */
    public native void putObjectVolatile(Object var1, long var2, Object var4);
    /**
    获取对象var1中偏移地址为var2、类型为int的字段值。此操作可以保证返回的值是内存中最新的值——可见性,即Volatile
    */
    public native int getIntVolatile(Object var1, long var2);
    /**
    设置对象var1中偏移地址为var2、类型为int的字段值为var4。此操作可以保证设置内存值之后值可见性,即Volatile
    */
    public native void putIntVolatile(Object var1, long var2, int var4);
    /**
    获取对象var1中偏移地址为var2、类型为boolean的字段值。此操作可以保证返回的值是内存中最新的值——可见性,即Volatile
    */
    public native boolean getBooleanVolatile(Object var1, long var2);
    /**
    设置对象var1中偏移地址为var2、类型为boolean的字段值为var4。此操作可以保证设置内存值之后值可见性,即Volatile
    */
    public native void putBooleanVolatile(Object var1, long var2, boolean var4);

    //其他getXXVolatile、setXXVolatile一样的意思,不一一说明
    
    /**
    释放被var1创建的一个在线程上的阻塞。这个方法可以被使用来终止之前调用var1导致的阻塞。
    这个方法不是线程安全的,同时这个是java代码而不是native代码
    */
    public native void unpark(Object var1);
    /**
    阻塞一个线程知道unpark出现、线程被中断、超时。如果一个unpark调用已经出现了,
   * 这里只计数。timeout为0表示永不过期.当isAbsolute为true时,
   * timeout是相对于新纪元之后的毫秒。否则这个值就是超时前的纳秒数。这个方法执行时
   * 也可能不合理地返回(没有具体原因)
    */
    public native void park(boolean isAbsolute, long time);

    public native int getLoadAverage(double[] var1, int var2);
    
    /**
    获取对象var1中偏移地址为var2的值,如果内存中的int值与期望的值var5相等,则设置内存中的
值为var4+var5
    */
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

    public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

    public final long getAndSetLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var4));

        return var6;
    }

    public final Object getAndSetObject(Object var1, long var2, Object var4) {
        Object var5;
        do {
            var5 = this.getObjectVolatile(var1, var2);
        } while(!this.compareAndSwapObject(var1, var2, var5, var4));

        return var5;
    }

    public native void loadFence();

    public native void storeFence();

    public native void fullFence();
    
    private static void throwIllegalAccessError() {
        throw new IllegalAccessError();
    }

    static {
        //注册
        registerNatives();
        Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
        //实例化
        theUnsafe = new Unsafe();
        // 获取各种类型数组的起始偏移地址及其增量地址
        ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class);
        ARRAY_BYTE_BASE_OFFSET = theUnsafe.arrayBaseOffset(byte[].class);
        ARRAY_SHORT_BASE_OFFSET = theUnsafe.arrayBaseOffset(short[].class);
        ARRAY_CHAR_BASE_OFFSET = theUnsafe.arrayBaseOffset(char[].class);
        ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset(int[].class);
        ARRAY_LONG_BASE_OFFSET = theUnsafe.arrayBaseOffset(long[].class);
        ARRAY_FLOAT_BASE_OFFSET = theUnsafe.arrayBaseOffset(float[].class);
        ARRAY_DOUBLE_BASE_OFFSET = theUnsafe.arrayBaseOffset(double[].class);
        ARRAY_OBJECT_BASE_OFFSET = theUnsafe.arrayBaseOffset(Object[].class);
        ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe.arrayIndexScale(boolean[].class);
        ARRAY_BYTE_INDEX_SCALE = theUnsafe.arrayIndexScale(byte[].class);
        ARRAY_SHORT_INDEX_SCALE = theUnsafe.arrayIndexScale(short[].class);
        ARRAY_CHAR_INDEX_SCALE = theUnsafe.arrayIndexScale(char[].class);
        ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale(int[].class);
        ARRAY_LONG_INDEX_SCALE = theUnsafe.arrayIndexScale(long[].class);
        ARRAY_FLOAT_INDEX_SCALE = theUnsafe.arrayIndexScale(float[].class);
        ARRAY_DOUBLE_INDEX_SCALE = theUnsafe.arrayIndexScale(double[].class);
        ARRAY_OBJECT_INDEX_SCALE = theUnsafe.arrayIndexScale(Object[].class);
        ADDRESS_SIZE = theUnsafe.addressSize();
    }
}

总的来说,UnSafe类提供的功能主要有:
1、内存管理:

  • 分配内存:allocateMemory
  • 扩展内存:reallocateMemory
  • 释放内存:freeMemory
    2、读写对象字段值
  • 读:getXXX、objectFieldOffset、getXXXVolatile等
  • 写:putXXX、putXXXVolatile等
    3、线程挂起和恢复操作
  • 挂起:park
  • 恢复:unpark
    4、CAS操作: compareAndSwapInt、compareAndSwapObject、compareAndSwapLong等

我们了解了CAS和UnSafe的原理之后,接着我们开始解析ConcurrentHashMap:
1、ConcurrentHashMap的数据结构是什么?
2、ConcurrentHashMap默认容量是多少?
3、ConcurrentHashMap最大容量是多少?
4、ConcurrentHashMap每次扩容是多少?
5、ConcurrentHashMap散列函数的实现是?如何计算出散列地址?
6、ConcurrentHashMap get的原理是?
7、ConcurrentHashMap put的原理是?
8、ConcurrentHashMap 是否是线程安全?
9、多线程环境下为什么ConcurrentHashMap比HashTable性能好?
10、ConcurrentHashMap与HashMap的区别是?

问题1:ConcurrentHashMap的数据结构是什么?

ConcurrentHashMap也是采用数组+单链表的数据结构,其使用的是内部类中的Node作为节点

/**
     * The array of bins. Lazily initialized upon first insertion.
     * Size is always a power of two. Accessed directly by iterators.
     */
    transient volatile Node<K,V>[] table;

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

通过上述代码,可以知道每个节点中hash值、key值是不可变的,而val和next是可见性的,即读写时均使用的内存中最新的值。

问题2:ConcurrentHashMap默认容量是多少?

与HashMap一样,其默认容量是16,看如下代码:

/**
     * The default initial table capacity.  Must be a power of 2
     * (i.e., at least 1) and at most MAXIMUM_CAPACITY.
     */
    private static final int DEFAULT_CAPACITY = 16;
问题3:ConcurrentHashMap最大容量是多少?

最大是其实是可以达到Integer.MAX_VALUE的,即使源码中定义了ConcurrentHashMap允许的最大容量,但是其在扩容的时候是不受这个最大容量的限制的,我们看下相关的源码:

private static final int MAXIMUM_CAPACITY = 1 << 30;

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        //n是当前散列表的大小
        int n = tab.length, stride;
                   //...省略
        //nextTab用于扩容时,存放移动或者拷贝原散列表元素,结束之后将nextTab赋值给table的
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                /**扩容大小每次为n*2,并没有使用最大值进行限制,所以时可以达到
                Integer.MAX_VALUE的
                */
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //...省略
      }
            //...省略
}

上述代码是在扩容时,默认扩容的大小为原来容量的2倍,且不受定义中最大容量的限制,所以其最大容量其实是可以达到Integer.MAX_VALUE的

问题4:ConcurrentHashMap每次扩容是多少?

每次扩容时原来的容量的2倍,即:n << 1。同时android中的ConcurrentHashMap与HashMap不一样有个扩容阈值和加载因子。

问题5:ConcurrentHashMap散列函数的实现是?如何计算出散列地址?

此问题在问题6中回答

问题6:ConcurrentHashMap get的原理是?

我们先看get的源码部分:

public V get(Object key) {
        /**
        tab:散列表
        e、p: 散列表中的节点
        n:散列表容量大小
        eh: 匹配节点的hash值
        ek: 匹配节点的key值
        h: 要查找的hash值
        */
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //通过spread函数来对key的hashcode继续计算,得到key的hash值
        int h = spread(key.hashCode());
        // 通过(n -1)& h得到映射地址即对于的散列表下标
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果散列表不为空,并且取下标处的节点不为空
            /**
            如果e代表的链表表头节点的hash值与要查找的has值相等,并且key相等,则代表表头节点就是查找
            的,直接返回此节点val值
            */
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                // 如果hash值小0,在以表头为e的链表中查找匹配的节点并返回节点的值
                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;
    }
// 对key的hashcode进行(h ^(h>>>16)) & HASH_BITS操作,得到key的hash值
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
/**
  通过UnSafe对象的getObjectVolatile方法去除数组tab中下标为((long)i << ASHIFT) + ABASE的节点,其
  具有Volatile语义,即同步内存的值,取到的节点是内存中最新的值
*/
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

get的原理总结来说是:
1、先计算出key的hash值:取key的hashcode进行(h ^ (h>>>16)) & HASH_BITS得到key的hash值
2、计算出映射地址及散列表的下标:对key的hash值进行h & (n - 1)操作得到映射地址
3、根据下标取出链表,再在链表中查找匹配的节点,如果找到节点则返回节点的val;如果找不到节点则返回null
到此我们可以很容易回答问题5中的问题:
散列函数的实现是:取key的hashcode,对hashcode进行(h ^ (h >>> 16)) & HASH_BITS操作得到hash值
是这样计算出映射地址的:对key的hash值进行h & (n - 1) 操作得到映射地址

问题7:ConcurrentHashMap put的原理是?

我们先看下ConcurrentHashMap中put的源码:

public V put(K key, V value) {
        return putVal(key, value, false);
    }
final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 如果key或者value为null则抛出NullPointerException
        if (key == null || value == null) throw new NullPointerException();
        //计算出key的hash值
        int hash = spread(key.hashCode());
        //链表大小
        int binCount = 0;
        // 遍历散列表
        for (Node<K,V>[] tab = table;;) {
            /**
            f: 散列表中的节点
            n: 散列表数组的长度
            i: 散列表的下标
            fh: 节点的hash值
            */
            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) {
                /** 
                如果散列表中下标为i的节点为空,即散列表中不存在此键值对,则先创建节点,通过cas操作
                将节点存入散列表的i位置,并退出循环
                */
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                //如果散列表正在移动,及扩容操作,则使用helpTransfer方法协助其他线程一同扩容
                tab = helpTransfer(tab, f);
            else {
                // 节点的旧值
                V oldVal = null;
                synchronized (f) {
                    // 从新从内存中取散列表i位置的节点
                    if (tabAt(tab, i) == f) {
                        //如果内存中最新的节点与f相等,则进行下列操作
                        if (fh >= 0) {
                            //如果hash值大于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) {
                            // 如果节点是树形结构,则通过树的方式进行put操作,在此不做说明
                            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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 进行扩容处理
        addCount(1L, binCount);
        return null;
    }

put的原理总结如下:
1、计算出key的hash值
2、通过key的hash值计算出其对应的映射地址及散列表的下标
3、取出散列表下标处的链表,如果链表为空,代表是新的元素,通过cas进行添加操作
4、如果散列表正在扩容,则协助其他线程一起完成扩容操作,再重新进行开始步骤3的操作
5、接着通过遍历链表,判断是否已经存在要插入的元素,如果存在则更改此元素的值并返回旧的值
6、如果在链表中没有找到要插入的元素,则通过cas进行添加操作
7、如果是添加元素则还需要根据需要进行扩容操作

问题8:ConcurrentHashMap 是否是线程安全?

答案是肯定的,ConcurrentHashMap是线程安全的。通过上述代码可以看到ConcurrentHashMap使用了Volatile、cas、synchronized的语义,采用锁分离机制确保线程的安全。其中get的操作时通过volatile来确保读取的是内存中最新的值;put操作通过volatile找出对应的链表,通过cas的方式创建链表;通过synchronized遍历链表并进行元素值的更改或者添加元素操作。

问题9:多线程环境下为什么ConcurrentHashMap比HashTable性能好?

问题8中的分析,而HashTable加锁机制是在每个涉及读写操作的方法都使用synchronized修饰。所以ConcurrentHashMap的get操作是具有并发性的,put操作则具有局部并发功能,所以ConcurrentHashMap的性能远比HashTable好。

问题10:ConcurrentHashMap与HashMap的区别是?

主要区别是:
1、ConcurrentHashMap是线程安全类,而HashMap不是。所以在单线程中ConcurrentHashMap性能比HashMap的性能差,但ConcurrentHashMap在多线程中时安全的而HashMap在多线程是不是安全的
2、ConcurrentHashMah不允许key和value为null,而HashMap是允许的
3、ConcurrentHashMap与HashMap默认容量都是16
4、ConcurrentHashMap与HashMap每次扩容默认都是原来容量的2倍

相关文章

网友评论

      本文标题:ConcurrentHashMap解析

      本文链接:https://www.haomeiwen.com/subject/omcslqtx.html