JUC集合(一)

今天利用下班之后的一点时间来整理一下之前看过的有关JUC集合的东西吧。

目录:

1.List和Set
2.Map
3.延伸内容

在JUC List和Set集合中,比较常见的有三个

  • CopyOnWriteArrayList
  • ConcurrentSkipListSet
  • ConcurrentHashMap

CopyOnWriteArrayList

其中CopyOnWriteArrayList,简单看一下它的声明:

CopyOnWriteArrayList implements List<E>, RandomAccess, Cloneable, java.io.Serializable

再看一下它内部的两个重要成员变量:

transient volatile Object[] array,
transient ReentrantLock lock = new ReentrantLock();

一个是volatile类型的对象数组,一个是一个可重入锁ReentrantLock

我们知道,无论是ArrayList还是CopyOnWriteArrayList,其内部的实现原理都是基于一个对象数组,那么在ArrayList类中,其内部的对象数组并没有用volatile来修饰,那么CopyOnWriteArrayList内部的这个数组为什么要用该关键字来修饰呢?
这个时候就要想到这个关键字的作用了:为了保证每个线程在对该数组进行操作时,总能看到其它线程对该volatile变量最后的写入。这样一来,就能够完全保证每个线程读取到该数组的值都是修改之后,最新的。(但是它并没有使线程同步的功能)
另外多说一点个人理解,之前看过一些java内存模型,这个关键字之所以有上述的作用,其实是由于它能够插入一些内存屏障来阻止指令重排序,所以才能起到上述的作用。

那么ReentrantLock在这里的作用是什么呢?
既然它是锁嘛,所以起的肯定就是一个同步的作用。

那么将这两个属性放到一起来理解该CopyOnWriteArrayList是如何保证线程安全,做到同步的呢?
首先ReentrantLock排它锁保证每一次只能有一个线程对其内部的数据进行访问,这首先保证了互斥,保证 了临界资源的安全性。
接着某个线程对其进行了操作之后,由于在java内存模型中,每个线程都有自己独立的内存空间,也叫线程缓存。而所有线程共享访问的是主存区域的变量、资源。所以当某个线程对某个资源进行了操作之后,如果没有及时更新到主存中去,那么其他线程访问的这个资源则就是已过期的,无效的。这个时候关键字volatile通过在底层加入内存屏障,防止指令重排序,来保证每个线程在对该数组进行操作时,总能看到其它线程对该volatile变量最后的写入。(注:有关volatile关键字的具体原理大家可以去看一下java内存模型)
这样最终就做到了线程之间的同步以及资源的安全。

CopyOnWriteArrayList特点:
在进行(add,set,remove)操作时需要重新复制一份原集合数据(Arrays.copyOf),然后再此基础上再进行操作,最后再与原集合合并。

所以,基于上边的那个特点,可以推测出CopyOnWriteArrayList的应用场景如下:

  1. CopyOnWriteArrayList尽可能的用在读操作多于写操作的业务逻辑中
  2. 由于其内部有一个锁ReentrantLock,所以支持并发操作,能够保证线程安全。
  3. 其迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  4. 使用迭代器(在该类中,迭代器的类型为COWIterator)进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

接着从add方法源码的角度来分析一下,顺便提高一下自己阅读代码的能力,啊哈哈:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 获取“锁”
    lock.lock();
    try {
        // 获取原始”volatile数组“中的数据和数据长度。
        Object[] elements = getArray();
        int len = elements.length;
        // 新建一个数组newElements,并将原始数据拷贝到newElements中;
        // newElements数组的长度=“原始数组的长度”+1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 将“新增加的元素”保存到newElements中。
        newElements[len] = e;
        // 将newElements赋值给”volatile数组“。
        setArray(newElements);
        return true;
    } finally {
        // 释放“锁”
        lock.unlock();
    }
}

实现原理无非就以下几步:

  1. 首先获取一个Lock,然后进行加锁操作。
  2. 然后调用getArray()方法来获取原来的数据集合,
  3. 调用Arrays.copyOf()方法来复制一个新的数组,该数组长度+1
  4. 然后将新的元素放入到数组的最后一位
  5. 最后再调用setArray()方法来重新赋值给valatile数组

CopyOnWriteArraySet

首先来看一下这个类的声明:

CopyOnWriteArraySet extends AbstractSet<E>  implements java.io.Serializable

其内部的一个重要的成员属性:

final CopyOnWriteArrayList<E> al

由此可以推断,CopyOnWriteArraySet是由内部的CopyOnWriteArrayList实现的。其实事实也是如此。

所以CopyOnWriteArraySet所具有的特点其实就是CopyOnWriteArrayList的那几个特点:宜读不宜写,线程安全,迭代速度快,无remove方法。

由于CopyOnWriteArraySet不允许有重复的元素,那么来看一下其内部是如何实现的:

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

很明显,该方法其实是CopyOnWriteArrayList的addIfAbsent的方法实现,那么来看一下addIfAbsent的方法实现吧:

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :  addIfAbsent(e, snapshot);
    }

实现原理简单概括一下就两步:

  1. 首先获取一个原数组的快照,然后调用indexOf()方法来判断原数组中是否有E对象,
  2. 如果有,则返回false,否则添加到数组中。

ConcurrentHashMap

首先看一下ConcurrentHashMap类的声明:

ConcurrentHashMap extends AbstractMap<K,V>  implements ConcurrentMap<K,V>, Serializable

实现原理:锁分段
ConcurrentHashMap将哈希表分成许多片段(Segment),每一个片段除了保存哈希表之外,本质上也是一个“可重入的互斥锁”(ReentrantLock)。多线程对同一个片段的访问,是互斥的;但是,对于不同片段的访问,却是可以同步进行的。

在jdk1.7中,ConcurrentHashMap大致是这样被实现的:
一个ConcurrentHashMap对应着n个Segment片段,然后每个Segment片段又对应着n个HashEntry<K,V>。以get方法为例:

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;
    // 获取key对应的Segment片段。
    // 如果Segment片段不为null,则在“Segment片段的HashEntry数组中”中找到key所对应的HashEntry列表;
    // 接着遍历该HashEntry链表,找到于key-value键值对对应的HashEntry节点。
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        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;
}

实现过程概括为两步:

  1. 首先将Key值hash,然后获取key所对应的Segment片段。
  2. 如果Segment片段不为null,则在Segment片段的HashEntry数组中找到key所对应的HashEntry列表;遍历该HashEntry链表,找到于key-value键值对对应的HashEntry节点。

增加方法put:

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 获取key对应的哈希值
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    // 如果找不到该Segment,则新建一个。
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

实现过程大致如下:

  1. put()根据key获取对应的哈希值,再根据哈希值找到对应的Segment片段。如果Segment片段不存在,则新增一个Segment。
  2. 将key-value键值对添加到Segment片段中。

** 该类解决hash冲突的办法为:拉链法 **

通过上边的两个实现,再结合jdk1.8对其的优化,可以想到,利用segment片段来进行分段加锁访问,其实这种控制粒度相对来说还比较大,如果能够精确控制到某一个HashEntry上,那么这种效率就更高了。其实jdk1.8就是这样做的。

jdk1.8的优化有两方面的内容:
1.去掉了segment,直接采用Node<K,V> table来保存数据,用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
2.将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。

相关知识

1.解决冲突的其他方法:

  1. 开放地址法:如果冲突,则向后查找,直接有一个空位置
  2. 建立公共溢出区:多分配一块内存用以存放冲突的对象
  3. 再哈希法

2.红黑书参考资料:

  1. 史上最清晰的红黑树讲解(上)
  2. 深入理解红黑树及TreeMap源码实现

3.ConcurrentHashMap学习资料:

  1. 深入浅出ConcurrentHashMap(1.8)
  2. Java并发编程总结4——ConcurrentHashMap在jdk1.8中的改进

后记

并发集合框架是自己平时空闲时间里面边看博客边看代码学习的,今天稍作整理,来记录一下自己的学习过程和成果。不过还有许多并发类,比如ConcurrentSkipListMap没来得及看。抽空多复习一下自己总结的,同时也要保持一颗好奇心多去了解一些新奇的东西。我觉得我会做的越来越好的!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容