Java集合(三) -- 并发安全Map

·  阅读 3233

一:摘要概述

  • Collections并发安全实现
  • HashTable并发安全与效率
  • ConcurrentHashMap并发安全
  • HashMap的key和value为什么允许null但是HashTable和ConcurrentHashMap不允许

二:Collecitons并发安全实现

Collections工具类中提供了函数synchronizedMap实现线程安全,该函数会返回一个线程安全的类SynchronizedMap。该类实现线程安全的方式也简单粗暴就是synchronized关键字,锁的对象就是this这个map对象

    // SynchronizedMap是Collections静态内部类
    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }
    // 该方法为SynchronizedMap的put函数,位置Conllctions2587行
    // synchronized锁的mutex就是构造函数的this对象
    public V put(K key, V value) {
        synchronized (mutex) {return m.put(key, value);}
    }
复制代码

三:HashTable并发安全与效率

HashTable的并发安全实现较为简单,查看源码几个关键方法get、put都是直接在方法上使用synchronized关键字修饰保证并发安全。虽然synchronized在JDK1.6经过大幅度优化后性能有所提升,但是整个方法上锁的策略还是不可避免的效率低下

四:ConcurrentHashMap并发安全

ConcurrentHashMap是并发安全的一个Map,由此引发下列两个问题:

  • ConcurrentHashMap如何保证线程安全
  • ConcurrentHashMap函数get、put并发安全

4.1 并发安全保障

ConcurrentHashMap在JDK1.7的时候使用lock锁的方式保证segment段范围内线程安全,这样做锁粒度较大并发效率也并不是很高。JDK1.8时使用CAS+Synchronized的方式保证线程安全,这时候锁的粒度是一个哈希桶

4.2 Get函数并发安全

查询的线程安全用到的是volatile关键字保证内存可见性的特点,查看如下Node内部类发现其中的valnext都被volatile修饰,因为get函数不涉及到数据修改。当其它线程修改数据以后volatile保证了第一时间主内存与工作内存的同步,并发情况下就是线程安全的,不得不说这是一个经典的volatile用法

        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
复制代码

4.3 Put函数并发安全

volatile不保证原子性,所以在put函数中涉及到元素修改以后使用了CAS+Synchronized保证线程安全

当哈希桶为空时使用CAS乐观锁实现原子数据替换,具体代码如下,源码位置在1019

    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null,
                     new Node<K,V>(hash, key, value, null)))
            break; 
    }
复制代码

当哈希桶不为空时直接使用synchronized关键字对哈希桶桶上节点加锁,保证链表更新的线程安全,具体代码如下,源码位置在1027

    // f对象在源码1018行进行赋值,值就是哈希桶位置上的Node节点
    synchronized (f) {
        ...
    }
复制代码

五:Null值问题

HashMap中key和value都可以存储null值,但是HashTable和ConcurrentHashMap都不允许key为null。这个问题主要还是fail-fastfail-safe两种机制的区别,其中HashMap采用fail-fast,剩下两者采用fail-safe

5.1 fail-fast

迭代过程中如果容器内容发生改变则直接抛出ConcurrentModificationException异常,实现原理就是使用modCount这个记录Map结构修改次数的属性值与expectedModCount迭代开始就存储的值进行比较

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
复制代码

5.2 fail-safe

安全失败机制,这种机制使得本次读取的数据不是最新数据,因为是在原容器的拷贝上进行的迭代。当读取到null值时无法判断是不存在亦或是为空。这时候也无法调用contain(key)进行判断,自然也就不允许插入null值

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改