JavaSE-集合(Map)

本文详细介绍了Java中Map接口的几种实现类,包括HashMap、Hashtable、LinkedHashMap、TreeMap和ConcurrentHashMap。HashMap和Hashtable是线程不安全和线程安全的选项,而LinkedHashMap保持插入顺序,TreeMap则根据key排序。ConcurrentHashMap在多线程环境下提供高效的安全操作,使用分段锁或CAS+Synchronized策略保证并发安全性。文章还讨论了各种实现的特性、适用场景以及并发问题的解决方案。
摘要由CSDN通过智能技术生成

在这里插入图片描述

Map

特点:key 唯一,无序

基本API

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MyMap {
    public static void main(String[] args) {
        /**
         * Map<K, V>:----key----value "键值对"
         * API
         *
         * 增加:put() putAll()
         * 删除:clear() remove()
         * 修改:replace()
         * 查询:size() get() entrySet() keySet() values()
         * 判断:equals() isEmpty() containsValue() containsKey()
         */
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(0, "hello");// 存入的键值对都是由映射关系的
        map.put(15, "java");
        map.put(18, "python");
        map.put(5, "java");
        map.put(0, "php");
        System.out.println(map.keySet()); //=> [0, 18, 5, 15],返回此映射中包含的键的Set视图。
        System.out.println(map.entrySet());  //=> [0=php, 18=python, 5=java, 15=java],返回此映射中包含的映射的Set视图。
        System.out.println(map.values());  //=> [php, python, java, java],返回此映射中包含的值的Collection视图。

        System.out.println(map.containsKey(0));  //=> true
        System.out.println(map.containsValue("java"));  //=> true

        System.out.println(map.replace(0, "Json"));  //=> "php",执行key进行更新操作,返回替换前的数据
        System.out.println(map.replace(0, "js", "Javascript"));  //=> false,对执行的映射进行更新操作成功返回true,否则返回false

        System.out.println(map);  //=> {0=php, 18=python, 5=java, 15=java},特点:key唯一,无序
        System.out.println(map.size());  //=> 4
        System.out.println(map.get(0));

        // 遍历
        Set<Integer> keySet = map.keySet();
        for (Integer o :
                keySet) {
            System.out.println(map.get(o));
        }
        System.out.println("------------------");
        Collection<String> keySet2 = map.values();
        for (String o :
                keySet2) {
            System.out.println(o);
        }
        // 内部接口作为泛型
        // entrySet() 方法返回的是Set 类型,有泛型,泛型为:Entry 类型--->是Map 接口中的内部接口
        // 这个接口的泛型又有两个类型Integer ,String
        System.out.println("------------------");
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> o :
                entries) {
            System.out.println(o.getKey() + "-----" + o.getValue());
        }
    }
}

HashMap 实现类

特点:key 唯一,无序

基本API

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MyMap {
    public static void main(String[] args) {
        /**
         * Map<K, V>:----key----value "键值对"
         * API
         *
         * 增加:put() putAll()
         * 删除:clear() remove()
         * 修改:replace()
         * 查询:size() get() entrySet() keySet() values()
         * 判断:equals() isEmpty() containsValue() containsKey()
         */
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(0, "hello");// 存入的键值对都是由映射关系的
        map.put(15, "java");
        map.put(18, "python");
        map.put(5, "java");
        map.put(0, "php");
        System.out.println(map.keySet()); //=> [0, 18, 5, 15],返回此映射中包含的键的Set视图。
        System.out.println(map.entrySet());  //=> [0=php, 18=python, 5=java, 15=java],返回此映射中包含的映射的Set视图。
        System.out.println(map.values());  //=> [php, python, java, java],返回此映射中包含的值的Collection视图。

        System.out.println(map.containsKey(0));  //=> true
        System.out.println(map.containsValue("java"));  //=> true

        System.out.println(map.replace(0, "Json"));  //=> "php",执行key进行更新操作,返回替换前的数据
        System.out.println(map.replace(0, "js", "Javascript"));  //=> false,对执行的映射进行更新操作成功返回true,否则返回false

        System.out.println(map);  //=> {0=php, 18=python, 5=java, 15=java},特点:key唯一,无序
        System.out.println(map.size());  //=> 4
        System.out.println(map.get(0));

        // 遍历
        Set<Integer> keySet = map.keySet();
        for (Integer o :
                keySet) {
            System.out.println(map.get(o));
        }
        System.out.println("------------------");
        Collection<String> keySet2 = map.values();
        for (String o :
                keySet2) {
            System.out.println(o);
        }
        // 内部接口作为泛型
        // entrySet() 方法返回的是Set 类型,有泛型,泛型为:Entry 类型--->是Map 接口中的内部接口
        // 这个接口的泛型又有两个类型Integer ,String
        System.out.println("------------------");
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for (Map.Entry<Integer, String> o :
                entries) {
            System.out.println(o.getKey() + "-----" + o.getValue());
        }
    }
}

加入要使用引用类型作为key 要怎么做?
必须重写hashCode()和equals()方法

import java.util.*;

class Test{
    public int age;
    public String name;

    public Test(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Test)) return false;
        Test test = (Test) o;
        return age == test.age && name.equals(test.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public String toString() {
        return "Test{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

public class MyMap {
    public static void main(String[] args) {
        /**
         * 使用引用类型作为 key
         * 必须重写 hashCode()和 equals()
         */
        Map<Test, String> map2 = new HashMap<Test, String>();
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(13,"xiaoming"),"1");
        System.out.println("--------------------");
        System.out.println(map2); //=> {Test{age=13, name='xiaoming'}=1, Test{age=12, name='xiaoming'}=1}
        System.out.println(map2.size());  //=> 2
    }
}

源码分析:

public class MyMap {
    public static void main(String[] args) {
        Map<Integer, String> map = new HashMap<Integer, String>();
        map.put(0, "hello");
        System.out.println(map);	
    }
}

存储方式是主数组 + 链表

底层构造器与put 方法

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 装填因子
    // 这就是底层储存用的主数组
    // 这个数组的类型是 Node<K,V> 类对象
    transient Node<K,V>[] table;
    final float loadFactor;
    transient int size;
    int threshold;  // 接收承重
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    // 获取hashCode
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /**
    * 内部类对象
    */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//------- 哈希码(int)
        final K key;//------- 键值(Integer)
        V value;//------- 数据(String)
        Node<K,V> next;//------- 指向下一个结点(Node<Integer,String>)

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }

        // 重写toString方法
        public final String toString() { return key + "=" + value; }

        // 重写hashCode方法保证唯一性
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        // 重写equals方法保证唯一性
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;				//----0
        int oldThr = threshold;				//----0
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 进行数组扩容 double threshold  double newCap
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;				//----16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);				//----12
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;				//----12
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];				//----长度 16 的数组
        table = newTab;  // 长度为16的主数组
        if (oldTab != null) {
        	// 扩容后的数组对原数组进行转移
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 初始化主数组
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 在主数组某一位置上第一次添加元素
            tab[i] = newNode(hash, key, value, null);
        else {
            // 在主数组某一位置上多次添加元素
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
        	// 超出承重范围就重新扩容  threshold:12----24----48 ...
            resize();
        afterNodeInsertion(evict);
        return null;
    }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
}

底层get 方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 执行循环遍历数组通过key 进行查询
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

Hashtable 实现类

基本API 效果与HashMap 相同。

区别:

  1. JDK版本

    (1)HashMap:JDK1.2 效率高线程不安全

    (2)Hashtable:JDK1.0 效率低线程较安全

  2. 数据储存问题

    (1)Hashtable 不能储存null 数据(key && value)。

    (2)HashMap key 和value 都可以是null ,且key 的null 也是唯一的。

LinkedHashMap 实现类

特点:按照添加顺序排列,唯一

基本API 效果与HashMap 相同。

import java.util.*;

class Test{
    public int age;
    public String name;

    public Test(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Test)) return false;
        Test test = (Test) o;
        return age == test.age && name.equals(test.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public String toString() {
        return "Test{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

public class MyMap {
    public static void main(String[] args) {
        Map<Test, String> map2 = new LinkedHashMap<Test, String>();
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(13,"xiaoming"),"1");
        map2.put(new Test(80,"xiaoming"),"1");
        map2.put(new Test(55,"xiaoming"),"1");
        System.out.println("--------------------");
        System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1}
        System.out.println(map2.size());  //=> 4
    }
}

TreeMap 实现类

特点:key有序,唯一

基本API 效果与HashMap 相同。

注意如果key 是引用类型则该引用类型的类必须实现Comparable 接口,并且重写compareTo()方法或者使用内部类直接定义比较器.

  1. 实现Comparable 接口,并且重写compareTo()方法
import java.util.*;

class Test implements Comparable {
    public int age;
    public String name;

    @Override
    public int compareTo(Object o) {
        return this.age - ((Test) o).age;
    }

    public Test(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Test)) return false;
        Test test = (Test) o;
        return age == test.age && name.equals(test.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public String toString() {
        return "Test{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

public class MyMap {
    public static void main(String[] args) {
        Map<Test, String> map2 = new TreeMap<Test, String>();
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(12,"xiaoming"),"1");
        map2.put(new Test(13,"xiaoming"),"1");
        map2.put(new Test(80,"xiaoming"),"1");
        map2.put(new Test(55,"xiaoming"),"1");
        System.out.println("--------------------");
        System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1}
        System.out.println(map2.size());  //=> 4
    }
}
  1. 定义内部比较器
import java.util.*;

class Test {
    public int age;
    public String name;

    public Test(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Test)) return false;
        Test test = (Test) o;
        return age == test.age && name.equals(test.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public String toString() {
        return "Test{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

public class MyMap {
    public static void main(String[] args) {
        Map<Test, String> map2 = new TreeMap<Test, String>(new Comparator<Test>() {
            @Override
            public int compare(Test o1, Test o2) {
                return o1.age - o2.age;
            }
        });
        map2.put(new Test(12, "xiaoming"), "1");
        map2.put(new Test(12, "xiaoming"), "1");
        map2.put(new Test(13, "xiaoming"), "1");
        map2.put(new Test(80, "xiaoming"), "1");
        map2.put(new Test(55, "xiaoming"), "1");
        System.out.println("--------------------");
        System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1}
        System.out.println(map2.size());  //=> 4
    }
}

ConcurrentHashMap实现类

在 Java 中,HashMap 是非线程安全的,如果想在多线程下安全的操作 map,有哪些解决方法呢?

  • 第一种方法,使用Hashtable线程安全类;

  • 第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;

  • 第三种方法,使用并发包中的ConcurrentHashMap类;

Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!

相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!

Collections.synchronizedMap 里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!

使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!

ConcurrentHashMap 类所采用的是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图:
在这里插入图片描述

当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

JDK1.8 对 HashMap 做了改造,当冲突链表长度大于8时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!

我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:
在这里插入图片描述

JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

说了这么多,我们再一起来看看 ConcurrentHashMap 的源码实现。

JDK1.7 中的 ConcurrentHashMap Segment是什么呢?

Segment本身就相当于一个HashMap对象。

同HashMap一样,Segment包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

单一的Segment结构如下:

像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。 因此整个ConcurrentHashMap的结构如下:

可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

这样的二级结构,和数据库的水平拆分有些相似。

下面我们看看ConcurrentHashMap并发读写的几种情况?

Case1:不同Segment的并发写入(可以并发执行)

Case2:同一Segment的一写一读(可以并发执行)

Case3:同一Segment的并发写入

Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。

由此可见,ConcurrentHashMap 中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

接下来,我们就来看下ConcurrentHashMap读写的过程。

  1. get方法

为输入的Key做Hash运算,得到hash值。

通过hash值,定位到对应的Segment对象

再次通过hash值,定位到Segment当中数组的具体位置。

  1. put方法

为输入的Key做Hash运算,得到hash值。

通过hash值,定位到对应的Segment对象

获取可重入锁

再次通过hash值,定位到Segment当中数组的具体位置。

插入或覆盖HashEntry对象。

释放锁。

JDK1.8 中的 ConcurrentHashMap
虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

JDK1.8 中的ConcurrentHashMap 相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。

JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见性!

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

    Node(int hash, K key, V val, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }
    ...
}

下面来具体分析一下JDK1.8 中的 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
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (ConcurrentHashMap.Node<K, V>[] tab = table; ; ) {
            ConcurrentHashMap.Node<K, V> f;
            int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                // 如果tab未初始化或者个数为0,则初始化node数组
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                        new ConcurrentHashMap.Node<K, V>(hash, key, value, null)))
                    // 如果使用CAS插入元素时,发现已经有元素了,则进入下一次循环,重新操作;如果使用CAS插入元素成功,则break跳出循环,流程结束
                    break;
            } else if ((fh = f.hash) == MOVED)
                // 如果要插入的元素所在的tab的第一个元素的hash是MOVED,则当前线程帮忙一起迁移元素
                tab = helpTransfer(tab, f);
            else {
                // 如果这个tab不为空且不在迁移元素,则锁住这个tab(分段锁)
                // 并查找要插入的元素是否在这个tab中
                // 存在,则替换值(onlyIfAbsent=false)
                // 不存在,则插入到链表结尾或插入树中
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // 如果第一个元素的hash值大于等于0(说明不是在迁移,也不是树)
                        // 那就是tab中的元素使用的是链表方式存储
                        if (fh >= 0) {
                            // tab中元素个数赋值为1
                            binCount = 1;
                            // 遍历整个tab,每次结束binCount加1
                            for (ConcurrentHashMap.Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    // 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false)
                                    // 并退出循环
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                ConcurrentHashMap.Node<K, V> pred = e;
                                if ((e = e.next) == null) {
                                    // 如果到链表尾部还没有找到元素
                                    // 就把它插入到链表结尾并退出循环
                                    pred.next = new ConcurrentHashMap.Node<K, V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        } else if (f instanceof ConcurrentHashMap.TreeBin) {  // 如果第一个元素是树节点
                            ConcurrentHashMap.Node<K, V> p;
                            // tab中元素个数赋值为2
                            binCount = 2;
                            // 调用红黑树的插入方法插入元素
                            // 如果成功插入则返回null
                            // 否则返回寻找到的节点
                            if ((p = ((ConcurrentHashMap.TreeBin<K, V>) f).putTreeVal(hash, key,
                                    value)) != null) {
                                // 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false)
                                // 并退出循环
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 如果binCount不为0,说明成功插入了元素或者寻找到了元素
                if (binCount != 0) {
                    // 因为上面把元素插入到树中时,binCount只赋值了2,并没有计算整个树中元素的个数
                    // 所以不会重复树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    // 如果要插入的元素已经存在,则返回旧值
                    if (oldVal != null)
                        return oldVal;
                    // 退出外层大循环,流程结束
                    break;
                }
            }
        }// 成功插入元素,元素个数加1(是否要扩容在这个里面)
        addCount(1L, binCount);
        // 成功插入元素返回null 
        return null;
    }

当进行 put 操作时,流程大概可以分如下几个步骤:

  1. 首先会判断 key、value是否为空,如果为空就抛异常;

  2. 接着会判断容器数组是否为空,如果为空就初始化数组;

  3. 进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;

  4. 在接着判断f.hash == -1是否成立,如果成立,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;

  5. 其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;

  6. 节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;

  7. 最后,插入完成之后,进行扩容判断。

initTable 初始化数组

    private final ConcurrentHashMap.Node<K, V>[] initTable() {
        ConcurrentHashMap.Node<K, V>[] tab;
        int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) // 如果sizeCtl<0说明正在初始化或者扩容,让出CPU
                Thread.yield();
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                // 如果把sizeCtl原子更新为-1成功,则当前线程进入初始化
                // 如果原子更新失败则说明有其它线程先一步进入初始化了,则进入下一次循环
                // 如果下一次循环时还没初始化完毕,则sizeCtl<0进入上面if的逻辑让出CPU
                // 如果下一次循环更新完毕了,则table.length!=0,退出循环
                try {
                    // 再次检查table是否为空,防止ABA问题
                    if ((tab = table) == null || tab.length == 0) { // 如果sc为0则使用默认值16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;// 新建数组
                        @SuppressWarnings("unchecked")
                        ConcurrentHashMap.Node<K, V>[] nt = (ConcurrentHashMap.Node<K, V>[]) new ConcurrentHashMap.Node<?, ?>[n];
                        // 把tab数组赋值给table
                        table = tab = nt;
                        // 设置sc为数组长度的0.75倍
                        // n - (n >>> 2) = n - n/4 = 0.75n
                        // 可见这里装载因子和扩容门槛都是写死了的
                        // 这也正是没有threshold和loadFactor属性的原因
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 把sc赋值给sizeCtl,这时存储的是扩容门槛 
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

使用CAS锁控制只有一个线程初始化tab数组;

sizeCtl在初始化后存储的是扩容门槛;

扩容门槛写死的是tab数组大小的0.75倍,tab数组大小即map的容量,也就是最多存储多少个元素。

helpTransfer 协助扩容

    final ConcurrentHashMap.Node<K, V>[] helpTransfer(ConcurrentHashMap.Node<K, V>[] tab, ConcurrentHashMap.Node<K, V> f) {
        ConcurrentHashMap.Node<K, V>[] nextTab;
        int sc;
        // 如果tab数组不为空,并且当前tab第一个元素为ForwardingNode类型,并且nextTab不为空
        // 说明当前tab已经迁移完毕了,才去帮忙迁移其它tab的元素
        // 扩容时会把旧tab的第一个元素置为ForwardingNode,并让其nextTab指向新tab数组
        if (tab != null && (f instanceof ConcurrentHashMap.ForwardingNode) &&
                (nextTab = ((ConcurrentHashMap.ForwardingNode<K, V>) f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            // sizeCtl<0,说明正在扩容
            while (nextTab == nextTable && table == tab &&
                    (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                // 扩容线程数加1    
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    // 当前线程帮忙迁移元素     
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

操作步骤如下:

  1. 第1步,对 table、node 节点、node 节点的 nextTable,进行数据校验;

  2. 第2步,根据数组的length得到一个标识符号;

  3. 第3步,进一步校验 nextTab、tab、sizeCtl 值,如果 nextTab 没有被并发修改并且 tab 也没有被并发修改,同时 sizeCtl < 0,说明还在扩容;

  4. 第4步,对 sizeCtl 参数值进行分析判断,如果不满足任何一个判断,将sizeCtl + 1, 增加了一个线程帮助其扩容。

addCount 扩容判断

    private final void addCount(long x, int check) {
        ConcurrentHashMap.CounterCell[] as;
        long b, s;
        // 先尝试把数量加到baseCount上,如果失败再加到分段的CounterCell上
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            ConcurrentHashMap.CounterCell a;
            long v;
            int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                            U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            // 计算元素个数
            s = sumCount();
        }
        //检查是否需要扩容,默认check=1,需要检查
        if (check >= 0) {
            ConcurrentHashMap.Node<K, V>[] tab, nt;
            int n, sc;
            // 如果元素个数达到了扩容门槛,则进行扩容
            // 注意,正常情况下sizeCtl存储的是扩容门槛,即容量的0.75倍
            while (s >= (long) (sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                // rs是扩容时的一个标识
                int rs = resizeStamp(n);
                if (sc < 0) {// sc<0说明正在扩容中
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        // 扩容已经完成了,退出循环
                        break;
                    // 扩容未完成,则当前线程加入迁移元素中
                    // 并把扩容线程数加1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                } else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2)) // 进入迁移元素 
                    transfer(tab, null); // 重新计算元素个数  
                s = sumCount();
            }
        }
    }

操作步骤如下:

第1步,利用CAS将方法更新baseCount的值

第2步,检查是否需要扩容,默认check = 1,需要检查;

第3步,如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;

第4步,如果不在扩容,将sizeCtl更新为负数,并进行扩容处理。

以上就是整个put方法的流程,可以从中发现,里面大量的使用了CAS方法,CAS 表示比较与替换,里面有3个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,如果相等,就将新值更新到内存地址里,如果不相等,就继续循环,直到操作成功为止!

get方法

    public V get(Object key) {
        ConcurrentHashMap.Node<K,V>[] tab; ConcurrentHashMap.Node<K,V> e, p; int n, eh; K ek;
        // 计算hash
        int h = spread(key.hashCode());
        // 判断数组是否为空,通过key定位到数组下标是否为空;
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果第一个元素就是要找的元素,直接返回
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                // hash小于0,说明是树或者正在扩容
                // 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
                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;
    }

步骤如下:

  1. 第1步,判断数组是否为空,通过key定位到数组下标是否为空;

  2. 第2步,判断node节点第一个元素是不是要找到,如果是直接返回;

  3. 第3步,如果是红黑树结构,就从红黑树里面查询;

  4. 第4步,如果是链表结构,循环遍历判断。

reomve 方法

    public V remove(Object key) {
        return replaceNode(key, null, null);
    }
    final V replaceNode(Object key, V value, Object cv) {
        int hash = spread(key.hashCode());
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                boolean validated = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            validated = true;
                            for (Node<K,V> e = f, pred = null;;) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    V ev = e.val;
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        if (value != null)
                                            e.val = value;
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            setTabAt(tab, i, e.next);
                                    }
                                    break;
                                }
                                pred = e;
                                if ((e = e.next) == null)
                                    break;
                            }
                        }
                        else if (f instanceof TreeBin) {
                            validated = true;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                V pv = p.val;
                                if (cv == null || cv == pv ||
                                    (pv != null && cv.equals(pv))) {
                                    oldVal = pv;
                                    if (value != null)
                                        p.val = value;
                                    else if (t.removeTreeNode(p))
                                        setTabAt(tab, i, untreeify(t.first));
                                }
                            }
                        }
                    }
                }
                if (validated) {
                    if (oldVal != null) {
                        if (value == null)
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }

步骤如下:

  1. 第1步,循环遍历数组,接着校验参数;

  2. 第2步,判断是否有别的线程正在扩容,如果是一起扩容;

  3. 第3步,用 synchronized 同步锁,保证并发时元素移除安全;

  4. 第4步,因为 check= -1,所以不会进行扩容操作,利用CAS操作修改baseCount值。

提问
ConcurrentHashMap不能解决什么问题呢?

请看下面的例子:

    public static void main(String[] args) {
        private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        public void unsafeUpdate (Integer key, Integer value){
            Integer oldValue = map.get(key);
            if (oldValue == null) {
                map.put(key, value);
            }
        }
    }

这里如果有多个线程同时调用unsafeUpdate()这个方法,ConcurrentHashMap还能保证线程安全吗?

答案是不能。因为get()之后if之前可能有其它线程已经put()了这个元素,这时候再put()就把那个线程put()的元素覆盖了。

那怎么修改呢?

答案也很简单,使用putIfAbsent()方法,它会保证元素不存在时才插入元素,如下:

public void safeUpdate(Integer key, Integer value) { map.putIfAbsent(key, value); } 复制代码
那么,如果上面oldValue不是跟 null 比较,而是跟一个特定的值比如1进行比较怎么办?

也就是下面这样:

            public void unsafeUpdate (Integer key, Integer value){
                Integer oldValue = map.get(key);
                if (oldValue == 1) {
                    map.put(key, value);
                }
            }

这样的话就没办法使用putIfAbsent()方法了。

其实,ConcurrentHashMap还提供了另一个方法叫replace(K key, V oldValue, V newValue)可以解决这个问题。

replace(K key, V oldValue, V newValue)这个方法可不能乱用,如果传入的newValue是null,则会删除元素。

            public void safeUpdate (Integer key, Integer value){
                map.replace(key, 1, value);
            }

那么,如果if之后不是简单的put()操作,而是还有其它业务操作,之后才是put(),比如下面这样,这该怎么办呢?

        public void unsafeUpdate (Integer key, Integer value){
            Integer oldValue = map.get(key);
            if (oldValue == 1) {
                System.out.println(System.currentTimeMillis());                         
                /**
                 * 其它业务操作 
                 */
                System.out.println(System.currentTimeMillis());
                map.put(key, value);
            }
        }

这时候就没办法使用ConcurrentHashMap提供的方法了,只能业务自己来保证线程安全了,比如下面这样:

        public void safeUpdate (Integer key, Integer value){
            synchronized (map) {
                Integer oldValue = map.get(key);
                if (oldValue == null) {
                    System.out.println(System.currentTimeMillis());
                    /**
                     * 其它业务操作
                     */
                    System.out.println(System.currentTimeMillis());
                    map.put(key, value);
                }
            }
        }

这样虽然不太友好,但是最起码能保证业务逻辑是正确的。

当然,这里使用ConcurrentHashMap的意义也就不大了,可以换成普通的HashMap了。

我们不能听说ConcurrentHashMap是线程安全的,就认为它无论什么情况下都是线程安全的。

总结

虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!

在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于8时,会将链表转化成红黑二叉树结构。

在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值