java-jdk-Map

85 阅读12分钟

jdk-map

JUC

HashMap

java扩容因子DEFAULT_LOAD_FACTOR=0.75的作用,默认初始空间DEFAULT_INITIAL_CAPACITY=16(2的幂,每次扩容2倍)的原因

  • 16和2 => 便于&运算
  • 0.75 => loadFactory过大碰撞概率增大,loadFactory过小浪费存储空间;16*0.75=9。存储大于9时自动扩容;即 size> threshold = loadFactory * capacity即会发生扩容2倍的动作(rehashing操作)

red-black-tree

  • hashMap-JDK7之前是使用数组+琏表存储数据,
  • 在JDK8之后引入红黑树;某个琏表的长度length>8 && 数组长度总length<64使用琏表存储, link.length > 8 & array.length > 64时使用数,经验值根据伯松分布。

多线程扩容时发生条件竞争问题

  • jdk1.7 使用头插:可能出现琏表环,造成get()数据时死循环,put/rehash等所有操作都是头插
  • jdk1.8 使用尾插,解决条件竞争问题.
  • 注意,hanhMap不是线程安全,不能用在多线程环境

HashMap-jdk1.7

// 存储数组
// 1. 线程不安全 => 首先因为使用数组+琏表存储,数组是实例属性,没有加锁做同步
// 2. 线程不安全 => tranfer()函数的问题
transient Entry[] table;  
static class Entry<K,V> implements Map.Entry<K,V>{
    // 1. key尽量使用final => equals,hascode重写
    final K key;
    V v;
    Entry next;
    int hash;
}
static int indexFor(int hash, int length) {
    return hash & (length-1);
}
// 多线程下容易形成琏表环
// 解决1.加锁2.多线程分组3.copy-on-write
void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            // 1. 旧琏表头
            Entry<K,V> e = src[j];
            if (e != null) {
                // 释放旧Entry数组的对象引用
                src[j] = null;
                do {
                    // 记录下一个节点
                    Entry<K,V> next = e.next;
                    // 计算新位置
                    int i = indexFor(e.hash, newCapacity);
                    // 插入新位置的头
                    e.next = newTable[i];
                    newTable[i] = e;
                    // 处理下一个节点
                    e = next;
                } while (e != null);
            }
        }
    }

主要的问题

  1. 初始大小,扩容因子的选择
  2. 线程不安全的原因=> 主要是对实例数组的操作没有加锁同步,put不具有原子性
  3. 取模算法

头插操作

  1. 依次遍历数组;遇到连表从上到下遍历连表
  2. 链条头插入新连表
  3. put/transfer函数都是头插

所谓的线程安全的实现方法=>1. 共享的元素需要加锁 2. 不同的线程有自己的备份

HashMap-jdk1.8

与jdk1.7的区别

  1. 红黑树(8|64)
  2. 尾插
  3. 扩容后迁移,算法
static class Node<K,V> implements Map.Entry<K,V> {
  // 1. 使用final修饰关键field(key,hash) => 用于计算hashcode,equals,不能随便被修改
  final int hash;
  final K key;
  V value;
  Node<K,V> next;
}
HashMap{
    transient int size;     // 逻辑长度
    transient int modCount;    // 修改次数
    transient Node<K,V>[] table;     // hash数组
    int threshold;     // 扩容临界值
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;     // 模式初始大小
    static final int MAXIMUM_CAPACITY = 1 << 30;  // 最大值是1<<31 - 1, 50%的空间利用率
    static final float DEFAULT_LOAD_FACTOR = 0.75f;     // 扩展因子
    // 扩展因子
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;
}
// hash改动
static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// @param onlyIfAbsent if true, don't change existing value,不存在时插入
// @param evict if false, the table is in creation mode.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 1.null先扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 2. 数组不存在,则插入
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 3. 是琏表第一个节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                // 4. tree
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 5. 琏表遍历
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // 6. 琏表结尾插入=>尾插
                        p.next = newNode(hash, key, value, null);
                        // 7. 转树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 8.琏表上存在,覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 8.琏表上存在,覆盖
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 9.最后扩容
        if (++size > threshold) resize();
        afterNodeInsertion(evict);
        return null;
}
// get
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) 
        {
            // 1. 琏表头,返回
            if (first.hash == hash &&
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 2. tree
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                  // 4. 遍历琏表
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
  1. HashMap使用Node存储单元
  2. 先使用k.hashcode()(hashcode是内存地址)计算数组索引index = (length-1) & hashcode
  3. 发生hash碰撞时,解决方法:开地址法,拉链法(java默认);
  4. 通过e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))判断是否插入
  5. 插入使用尾插法;put/rehash都是。

<k1,v1>,<k2,v2>发生hash碰撞的情况时get/put的流程

index(k1.hashcode()) = index(k2.hashcode())计算索引时发生hash碰撞。

  1. 发生碰撞,会存放在相同的bucket,再使用琏表依次存储Node<k,v>对象.如果k.equals(k1) && k1 == k2相等则覆盖,不相等则尾插入;
  2. 读取时先计算k.hashcode()计算bucket,在使用k.equal()做值比较找到指定的对象.

重写equals方法的时候,一定要重写hashCode方法;equal()默认使用hashcode()

对于<k1,v1>,<k2,v2>; 如果k1.equals(k2)则,说明k1,k2是相同对象,应该存在map.get(k1) = map.get(k2); 如果不重写hashcode()计算索引时会得到不同index,则put(k1,v1)和put(k2,v1)会放到不通的bucket;这里是矛盾的。

作为key的值的对象final定义,重写equals(),hashCode()函数,避免出现碰撞;如Integer,String做Key.使用fianl修饰计算hashcode()的field.

jdk1.7-bug

条件竞争

  • jdk7头插,插入时新琏表中的相对顺序与原来的琏表相反,多线程容易生成琏表环
  • 尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题

HashMap-HashTable-ConcurrentHashMap

HashMap: 非synchronized,可以key=null,普通的hash数据结构, HashTable: 使用synchronized修饰方法,不可以key=null; Collections.synchronizedMap,使用synchronzed代码块,修饰final Object(封装了has-a) ConcurrentHashMap:使用分段锁

  • HashMap的迭代器(Iterator)是fail-fast
  • Hashtable的enumerator不是fail-fast的

HashMap-HashSet-TreeMap

HashMap,TreeMap实现Map HashSet实现Set

ConcurrentHashMap

线程安全的选项

  • Collections.synchronizedMap(Map)
  • Hashtable
  • ConcurrentHashMap

Collections.synchronizedMap(Map)

// 使用对象锁实现,使用的是synchronized的代码块
SynchronizedMap(Map<K,V> m) {
    this.m = Objects.requireNonNull(m);
    mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
    this.m = m;
    this.mutex = mutex;
}
public int size() {synchronized (mutex) {return m.size();}}

Hashtable

// 使用的是synchronized的方法
public synchronized int size() {return count;}

HashTable对key,value是否为null做了判断,如果是则抛出异常.底层原因是fail-safe.

failFast-failSafe

在Collection集合和JUC的各个集合类中,有线程安全和线程不安全这2大类的版本。

  • 线程不安全的类,并发情况下可能会出现fail-fast情况
  • 线程安全的类,可能出现fail-safe的情况.... 安全的就safe.....

集合的迭代器Iterator的实现原理是对原始的list和modCount(修改次数)拷贝了一份快照。

  • 如果对线程不安全的类进行并发的修改,modCount不是期望的值会抛出异常
  • 如果对线程安全的类进行遍历,copy的过程中无法判断null代表的是不存在还是空,copy的数据就会出现问题。

对比

  • 实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。
  • 初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
  • 扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
  • 迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的
  • null值存取不同,hashtable是fail-safe的.

ConcurrentHashMap-jdk1.7

// 分段锁的实现
static final class Segment<K,V> extends ReentrantLock implements Serializable{
    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    // fail—safe
    transient int modCount;
    // 扩容界限
    transient int threshold;
    // 负载因子
    final float loadFactor;
}
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
     // 当用默认构造函数时,最大并发数是 16,即最大允许 16 个线程同步写操作
      private static final int DEFAULT_CAPACITY = 16;
    }
 // hash数组
 static final class HashEntry<K,V> {
     // final,volatile
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
 }
}

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null) throw new NullPointerException();
        // 1. 计算hash
        int hash = hash(key.hashCode());
        // 2. 计算segment索引
        // hash值是int值,32bits。segmentShift=28,无符号右移28位,剩下高4位,其余补0。
        // segmentMask=15,长度16,二进制低4位全部是1,所以j相当于hash右移后的低4位
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null)
            // 3. 使用segment[0]初始化segment[j]
            s = ensureSegment(j);
        // 4. 插入
        return s.put(key, hash, value, false);
    }

1.7 初始化

  • Segment数组长度为 16,不可以扩容
  • Segment[i] 的默认大小为 2,负载因子是 0.75,得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容
  • 这里初始化了 segment[0],其他位置还是 null,至于为什么要初始化 segment[0],方便使用segment[0]新建segment[i]
  • 当前 segmentShift 的值为 32 - 4 = 28,segmentMask=16 - 1 = 15,segment大小是16使用hashcode高4为计算

1.7 put

  1. k.hashcode高4位计算segment的index,segments只初始化了segments[0],其他的put时初始化
  2. segment[index]==null时,则cas创建,Unsafe使用segmens[]+index偏移地址做cas => 初始化segment使用cas
  3. hash计算segment中的index,加锁使用ReentrantLock插入类似Map => 插入使用Lock

ConcurrentHashMap-jdk1.8

/**
* 通过值控制Map的状态
* 0: 默认值,-1:正在初始化table,-n:n-1个线程在扩容
* table = null,未初始化:是需要初始化的大小,default=0
* table != null,初始化完成: 是length*0.75
**/
private transient volatile int sizeCtl;
// buckets
transient volatile Node<K,V>[] table;
// 扩容时用的buckets => 扩容时使用,有点类似redis
private transient volatile Node<K,V>[] nextTable;
// put底层实现
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

//  计算hash
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 不允许null
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 1. 先初始化buckets(tab)
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                // tabAt = table_at
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 2. bucket的头节点==null,直接cas设置(unsafe工具),casTabAt=cas_table_at
                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)
                // 3. 判断正在扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 4. bucket头节点存在,直接synchronized保护整个琏表, 此时是hash冲突
                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) {
                    // 琏表长度大于8
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
// 琏表长度>8则调用    
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        // MIN_TREEIFY_CAPACITY 为 64
        // 1. 所以,如果数组长度小于 64 的时候,其实也就是 32 或者 16 或者更小的时候,会进行数组扩容
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        // 2. 转树
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            // 加锁
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    // 下面就是遍历链表,建立一颗红黑树
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    // 将红黑树设置到数组相应位置中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}

put流程

  1. 如果没有初始化就先调用 initTable()方法来进行初始化过程(unsafe)
  2. 如果没有 hash 冲突就直接 CAS 插入头节点(unsafe)
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在 hash 冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
  5. 最后一个如果该链表的数量大于阈值 8,就要先转换成黑红树的结构,break 再一次进入循环,如果添加成功就调用 addCount()方法统计 size,并且检查是否需要扩容

transfer

阅读源码之前,先要理解并发操作的机制。原数组长度为 n,所以我们有 n 个迁移任务,让每个线程每次负责一个小任务是最简单的,每做完一个任务再检测是否有其他没做完的任务,帮助迁移就可以了,而 Doug Lea 使用了一个 stride,简单理解就是步长,每个线程每次负责迁移其中的一部分,如每次迁移 16 个小任务。所以,我们就需要一个全局的调度者来安排哪个线程执行哪几个任务,这个就是属性 transferIndex 的作用。

第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指代了第二个线程,也可以是同一个线程

concurrenthashmap_put

jdk1.8中cas实现的原理

  1. 了解Unsafe实现cas的原理(cpu-cas)
  2. JDK1.8中使用synchronized + CAS(Unsafe类)实现来更高效的对map中每个Node的细粒度加锁,cas对第一个元素操作;
  3. JDK1.7segment+cas,用来初始化segemnt

jdk1.8针对synchronized的优化

  1. 无锁,轻量锁,cas,synchronized,
  2. jdk1.7的Segment继承ReetantLock
  3. jdk1.8替换为sychronized代码块,性能更好

扩容条件

  1. table中节点数大于length*0.75
  2. 琏表长度>8且table.length>64,构建tree

jdk1.8的的死锁

Map<String, Integer> map = new ConcurrentHashMap<>(16);
// must not attempt to update any other mappings of this map.
// 不要同时更新2个
map.computeIfAbsent("AaAa",key ->map.computeIfAbsent("BBBB",key2 -> 42));

不是很重要(还需要研究) blog.csdn.net/zhanglong_4…

CopyOnWriteArrayList

  • CopyOnWriteArrayList,通过 has-a ReentrantLock实现同步,好想不是使用cow实现的同步.....暂时不确定
  • Vector使用synchronized修饰方法

LinkedHashMap

  1. is-a HashMap
  2. Entry继承为支持连表
  3. 模版函数修改顺序

HashSet

  1. has-a HashMap
  2. put函数(HashSet的value存入到HashMap的Key)
  3. 使用时注意equals()

ArrayList,Vector,LinkedList

  1. Vector,java遗留容器(Vector,hastTable,Dictionary,Stack,Properties,BitSet...不推荐使用); synchronized实现线程安全
  2. 线程安全的工具; synchronizedList

collection

  • Collection
    • List: ArrayList,LinkedList
    • Set: HashSet,LinkedHashSet,TreeSet
    • Queue:
  • Map
    • HashMap-LinkedHashMap
    • TreeMap
    • ConcurrentHashMap
  • Queue: 本身有线程安全的要求
  • TreeSet: 是平衡树
  • HashMap
    • 线程不安全 => Collections.synchronizedMap() 或 ConcurrentHashMap
    • 最多只允许一个key=null
    • jdk7: 数组 + 单链表(解决hash冲突)
    • jdk8: 数组 + 单链表 + 红黑树(是一种平衡树,链表长度>8编程则红黑树)
  • ConcurrentHashMap
    • 通过Segment实现分段加锁,默认16个
    • Segment继承了ReentranLock
  • HashTable
    • 继承Dictionary
    • 通过sychronized实现线程安全

ps