`
tmj_159
  • 浏览: 708613 次
  • 性别: Icon_minigender_1
  • 来自: 永州
社区版块
存档分类
最新评论

juc 下的集合之二 (ConcurrentHashMap)(JDK1.8版本)

 
阅读更多

为什么要再写一篇ConcurrentHashMap的文章,有下面几个原因:

1. jdk1.8 和我上次写的1.6版本的在实现上差距很大,我也是今天看了下才发现,去年又一次去面试刚好问道这个地方了,我就胸有成竹的回答了有关同步,锁,效率的问题,今天一看基本全错了。

2. 上次写的文章有点笼统,没有触及到问题的根本,只是在代码层面走了流程,这次我需要把没完成的问题一一解决。

 

所以此文会解决如下几个问题:

1. 基本结构是什么样的,关键的操作(增加,修改,删除会如何影响结构)

2. 既然遍历可以不像HashTable一样出异常,那么它是怎么做到的

3. 同样比HashTable快,又快在哪里,为什么会快呢

 

一、基本结构&基本操作

 我们都知道HashMap的数据结构是数组+链表的结构,我之前认为高端一点的说法应该是邻接表的结构,但是我上网搜了下很少有人直接说是邻接表。

//数组
/**
     * 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;

    /**
     * The next table to use; non-null only while resizing.
     */
    private transient volatile Node<K,V>[] nextTable;

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

 这里会有两个数组的情况,为什么会这样呢,注释上说的清楚,当扩张大小的时候会用到这张表,比如说之前有16个元素的数组,当第16 * LOAD_FACTOR +1 = 16* 0.75 +1 = 12 +1 = 13的时候,会新建一个数组大小为16 << 1 = 32 ,同样下一次的变动大小为 25。如果你想看jdk中是如何变动的,可以看下面的代码

 

//方法调用为 putVal() -> addCount() -> transfer()

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//看这里
......
}

 

所以添加的时候,会根据key的hash值,映射到当前数组的某一个元素也就是Node,然后在从当前Node找key为新添加元素的k,如果等于表示已经存在key,进行更新操作,否则进行添加操作。添加完之后在看size()是否达到扩展的情况,如果达到了执行上面扩大数据的操作了。这里注意一下,新版本的hash映射到数组元素的算法已经修改了。

/**
     * Spreads (XORs) higher bits of hash to lower and also forces top
     * bit to 0. Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        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) { //tabAt 也是新的
                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)
                tab = helpTransfer(tab, f);
            else {
                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) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

tabAt 是根据hash 定位的方法,源代码如下

/*
     * Volatile access methods are used for table elements as well as
     * elements of in-progress next table while resizing.  All uses of
     * the tab arguments must be null checked by callers.  All callers
     * also paranoically precheck that tab's length is not zero (or an
     * equivalent check), thus ensuring that any index argument taking
     * the form of a hash value anded with (length - 1) is a valid
     * index.  Note that, to be correct wrt arbitrary concurrency
     * errors by users, these checks must operate on local variables,
     * which accounts for some odd-looking inline assignments below.
     * Note that calls to setTabAt always occur within locked regions,
     * and so in principle require only release ordering, not
     * full volatile semantics, but are currently coded as volatile
     * writes to be conservative.
     */

    @SuppressWarnings("unchecked")
    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);
    }

 U 是一个sun.misc.Unsafe 的实例,Unsafe是一个低级别的,不安全的方法集合,主要用在一些多线程处理方面的优化,绝大部分是native的方法。

 

二、安全的遍历如何做到

 说到遍历我们用的最多的是下面这种方式了

ConcurrentHashMap<K,V> map ;
map.entrySet().iterator();
while(iterator.hasNext){
    iterator.next();
}

 接下来我们看看,entrySet()这个方法

public Set<Map.Entry<K,V>> entrySet() {
        EntrySetView<K,V> es;
        return (es = entrySet) != null ? es : (entrySet = new EntrySetView<K,V>(this));
    }

 方法中使用的是EntrySetView这个类包装的,注意到参数this,表示把当前的Map传递过去了的,为什么传递过去呢,很简单就为了得到map中的数组和链表,也就是数据。接下来看看EntrySetView这个类

static final class EntrySetView<K,V> extends CollectionView<K,V,Map.Entry<K,V>>
        implements Set<Map.Entry<K,V>>, java.io.Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        EntrySetView(ConcurrentHashMap<K,V> map) { super(map); }
......
//迭代器方法如下
public Iterator<Map.Entry<K,V>> iterator() {
            ConcurrentHashMap<K,V> m = map;
            Node<K,V>[] t;
            int f = (t = m.table) == null ? 0 : t.length;
            return new EntryIterator<K,V>(t, f, 0, f, m);
        }
......
}

迭代器也有一个包装类来完成EntryIterator, 同样注意这个类的构造方法有5个参数(map中的节点数组,数组的长度,0,数组的长度,map本身的引用)。看看这个看似简单的类是如何工作的。

static final class EntryIterator<K,V> extends BaseIterator<K,V>
        implements Iterator<Map.Entry<K,V>> {
        EntryIterator(Node<K,V>[] tab, int index, int size, int limit,
                      ConcurrentHashMap<K,V> map) {
            super(tab, index, size, limit, map);
        }

        public final Map.Entry<K,V> next() {
            Node<K,V> p;
            if ((p = next) == null)
                throw new NoSuchElementException();
            K k = p.key;
            V v = p.val;
            lastReturned = p;
            advance();
            return new MapEntry<K,V>(k, v, map);
        }
    }

 这里只是把next node的值取出来包装到MapEntry中返回就好,具体如何找next呢,关键来了,看advance()这个方法。

/**
         * Advances if possible, returning next valid node, or null if none.
         */
        final Node<K,V> advance() {
            Node<K,V> e;
            if ((e = next) != null)
                e = e.next;
            for (;;) {
                Node<K,V>[] t; int i, n;  // must use locals in checks
                if (e != null)
                    return next = e;
                if (baseIndex >= baseLimit || (t = tab) == null ||
                    (n = t.length) <= (i = index) || i < 0)
                    return next = null;
                if ((e = tabAt(t, i)) != null && e.hash < 0) {//特殊类型的节点hash值都为负数
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        e = null;
                        pushState(t, i, n);
                        continue;
                    }
                    else if (e instanceof TreeBin)
                        e = ((TreeBin<K,V>)e).first;
                    else
                        e = null;
                }
                if (stack != null)
                    recoverState(n);
                else if ((index = i + baseSize) >= n)
                    index = ++baseIndex; // visit upper slots if present
            }
        }

 上面代码注意下面这一部分

if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        e = null;
                        pushState(t, i, n);
                        continue;
                    }

代码大意很明确,意思是如果当前节点Node,是ForwardingNode的类型的时候,就使用nextTable来当作当前的table, 在最前面的数据结构中我们提到了有两个table,另一个table只有当调整空间也就是resize的时候使用,此类中与resize相关的方法transfer(tab,nextTab)在最前的时候也已经讲过了,这里再多贴出点东西,看看关于ForwardingNode相关的东西。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                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;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);//这里
......
}

ForwardingNode表示正在移动的节点,并且此类节点的Hash值都为-1 , 

到这里大致清楚了,任何数组大小调整的时候都会有两个表,一个表应对各种外界操作,一个表应对内部大小调整,调整完全好了,才会同步到主表中。

 

三、快在哪里

       要比较快在哪里得看在什么情况下(是否并发访问),和谁比较(HashMap,还是HashTable,这里既然说并发情况下肯定是和HashTable)比较了。

      说上面一段话是想说明,任何数据类型都有一定的限制性,我很多次面试的时候问过,为什么ConcurrentHashMap要好,給我的回答是新出来的,然后反问一句,如果不好为什么要新写一个出来,弄的我也是一愣的。

      说正事,通过上面的说明,ConcurrentHashMap比HashTable快的两个地方是

 

      1.  更细粒度的锁

HashTable是通过在每个方法上加Synchronized来完成同步的,而ConcurrentHashMap是在某个Node列上(某个数组节点,里面包含了链表)加锁来实现同步。相比之下增加了锁的个数从而提高并发的数量,但同时锁的管理也更加复杂,锁的消耗也更加大,其实这也是为什么不在每一个节点(每个列上的每个子节点)上都加锁的原因。

 

       2. 空间换时间

ConcurrentHashMap 通过增加额外的副本,来避免最耗时的resize操作,这样有两个好处一是将耗时的resize操作让其它的线程处理,二是任何时间遍历都不会出现数据不全的情况,同时不用额外锁住整个map。

 

 

 

分享到:
评论

相关推荐

    JDK1.8:JDK1.8源码解析-源码解析

    JDK1.8是Oracle公司发布的Java 8版本,引入了许多重要的新特性,如Lambda表达式、Stream API以及对Java并发库的重大改进。 **1. Lambda表达式** JDK1.8引入了Lambda表达式,这是一种简洁的函数式编程特性,可以用于...

    Java 多线程与并发(13-26)-JUC集合- ConcurrentHashMap详解.pdf

    【Java 多线程与并发】并发集合类`ConcurrentHashMap`是Java程序设计中一个重要的工具,尤其在高并发场景下,它提供了高效的线程安全。`ConcurrentHashMap`在JDK 1.7和1.8中有着显著的区别。 在JDK 1.7中,`...

    joeylv#joscrapy#【目录】JUC集合框架目录1

    JUC集合框架的目录整理如下:1. 【JUC】JUC集合框架综述【JUC】JDK1.8源码分析之ConcurrentHashMap(一)【JUC】JDK1.8源

    Java并发包源码分析(JDK1.8)

    Java并发包源码分析(JDK1.8):囊括了java.util.concurrent包中大部分类的源码分析,其中涉及automic包,locks包(AbstractQueuedSynchronizer、ReentrantLock、ReentrantReadWriteLock、LockSupport等),queue...

    艾编程coding老师:JUC 并发编程 + 底层原理.pdf

    JUC从JDK 1.5版本开始被引入,并随着后续版本的更新不断丰富和完善。JUC的出现极大地提升了Java多线程编程的能力和效率,使得并发编程更加简便和安全。 并发编程是指多线程或多进程同时操作共享资源,提高CPU利用率...

    JavaCommon:Java基础用法,集合,线程,JUC,jdk5--8各个版本特性。

    该项目还深入解析了从JDK 5到JDK 8各版本的重要特性,为开发者提供了丰富的代码示例和源码分析。 1. **Java基础用法**: - **变量与数据类型**:Java支持基本数据类型如int、float、char等,以及引用类型如类、...

    尚硅谷大厂面试题第二季周阳主讲整理笔记

    在JDK1.8中,HashMap的初始化延迟到第一次put操作,底层结构变为Node数组,同时引入了红黑树,当链表长度达到8时,会转为红黑树以提高查询性能。 【JUC并发编程】 Java并发编程主要涉及java.util.concurrent包,...

    java知识储备.docx

    - **ConcurrentHashMap**:在并发环境下提供线程安全,采用分段锁策略,JDK1.8后使用CAS和Synchronized优化。 **4. JUC(Java并发工具包)** - **Volatile**:保证可见性,但不保证原子性,适合简单变量共享。 - **...

    thread_analysis:JDK中JUC学习记录

    1. ConcurrentHashMap:线程安全的HashMap替代品,提供了高效的并发读写性能,适合在高并发场景下使用。 2. ConcurrentLinkedQueue:无界的线程安全队列,基于链接节点的无锁实现,适用于低冲突的并发环境。 3. ...

    jdkLearning:阅读java原始代码包含:集合,JUC,Executor体系

    这个"jdkLearning"项目专注于深入理解JDK的源代码,特别是集合、并发编程(JUC)和Executor服务这三个核心领域。通过阅读和分析这些源码,开发者可以更深入地了解Java平台的工作原理,提升编程技能,并优化应用程序...

    JDK8Translation:学习Java8代码,了解Java常用类库的原理,该工程转换为JDK8常用的类库注释

    起因:由于之前经常会翻看JDK的源码,尤其是JUC包下面的一些类库,发现一些类中开头的大段英语注释写的非常好,很多时候都说明了设计思路以及这样做的原因,所以想把这些注释翻译成中文,便于自己温习的同时也方便...

    javajdk8源码-concurrencyJava:基于JDK8源码学习并发编程知识

    在JDK 8中,重点介绍了`java.util.concurrent`(JUC)并发工具包。这个包包含了许多高效并发工具,如`ExecutorService`、`Future`、`Callable`、`Semaphore`、`CountDownLatch`和`CyclicBarrier`等。这些工具可以...

    JVM_JUC_Demo:练习案例

    接着,我们转向JUC,它是Java 5及以上版本引入的一组并发编程工具,极大地简化了多线程编程。JUC主要包括以下组件: 1. **并发容器**:如`ConcurrentHashMap`,提供线程安全的哈希映射,比传统的`synchronized`机制...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第一阶段34讲、ThreadGroup API介绍之二.mp4 │ 高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │ 高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │ 高并发编程第一阶段37讲...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第一阶段34讲、ThreadGroup API介绍之二.mp4 │ 高并发编程第一阶段35讲、线程池原理与自定义线程池.mp4 │ 高并发编程第一阶段36讲、自定义个简单的线程池并且测试.mp4 │ 高并发编程第一阶段37讲...

    Java基础笔记(包括底层原理)

    Java是世界上最流行的编程语言之一,以其跨平台能力、面向对象特性、安全性以及高效性能而闻名。Java的基础知识包括了从语法、数据类型到高级特性的广泛内容。在深入理解Java时,了解JVM(Java虚拟机)的工作原理是...

    JUC基石——Unsafe类

    在Java并发编程领域,`Unsafe`类扮演着一个特殊的角色,尽管它被标记为“不安全”,但却是Java并发库(JUC)中的重要基石,尤其在高并发场景下,如`ConcurrentHashMap`、`Atomic`系列类、`AQS`...

    java并发编程经典书籍(英文版)

    - **并发集合**:详述了JUC(Java Util Concurrency)库,包括ArrayList、LinkedList、HashMap等线程安全的改进版本,以及CopyOnWriteArrayList、ConcurrentHashMap等高效并发集合。 - **原子类**:如...

    java技术储备,如何提升自己

    2. JDK 中的设计模式:了解 JDK 中的设计模式,包括 IO 中的装饰模式和设配器模式等。 3. 框架中的设计模式:了解常用的设计模式,例如 Struts 中的责任链模式、Spring 中的工厂模式、动态代理模式等。 数据结构 1...

    java 并发编程

    JDK中的并发框架JUC(java.util.concurrent)包括了线程池、并发容器、原子操作和并发工具类。JUC的设计目标是提供生产级别的线程安全的容器和类,减少同步问题的复杂性,并且尽量避免重复发明轮子。线程池是管理...

Global site tag (gtag.js) - Google Analytics