《职业复习系列-Java基础篇》

人生新篇章

  • Java基础篇
    • 数据结构
      • HashMap(JDK1.7&JDK1.8)
        • 为什么使用
        • 使用场景
        • 数据结构
          • 核心属性
        • 并发问题
          • JDK1.7 死链&数据丢失
            • 源码分析
          • JDK1.8
            • 源码分析
          • threshold
          • MAXIMUM_CAPACITY
          • DEFAULT_INITIAL_CAPACITY
      • ArrayList
      • LinkedList
    • 线程池 ThreadPoolExecutor
    • 并发 synchronized 关键字
    • 并发 volatile 关键字
    • 并发组件 ThreadLocal
    • 并发组件 AbstractQueuedSynchronizer

Java基础篇

数据结构

HashMap(JDK1.7&JDK1.8)

为什么使用

同时具备数据和链表的能力、基于Key - Value 的形式存储,可以满足大多数业务需求

使用场景

快速查找指定元素

数据结构

数组 + 链表 基于哈希表 拉链法解决Hash冲突

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间,具体可以参考 treeifyBin方法。

核心属性

JDK1.7添加元素流程

  1. 计算hash&hash值对应数组的index
  2. 循环该数组索引下面链表,如果hash&key 相等则覆盖返回旧值
  3. 还没有添加元素就modCount++
  4. 添加entry到指定数组位置

并发问题

JDK1.7 死链&数据丢失

并发问题的原因:

transient Entry[] table = (Entry[]) EMPTY_TABLE;

table 数组是所有线程都可以共享修改的

createEntry 方法

Entry e = table[bucketIndex]

如果两个线程同时执行到这,那另外一个线程的值就会被覆盖掉

resize 方法
resize(

 transfer()

table = newTable;     //其他线程数据丢失

)

死链问题

e & next 都是线程局部共享所以形成死链,当前循环也可以正常退出
《职业复习系列-Java基础篇》_第1张图片
总结:
1)死链问题主要是由于上文代码中e.next并多线程并发修改,不管是自己互链,两个对象互链还是数据丢失都是这个原因导致的
2)put() get() transfer() 运行到此slot槽上面CPU都会飙升(死循环)

源码分析
//梳理一下jdk1.7 hashmap 添加元素调用链
```java
//entry结构定义
 Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        
public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        //根据hash值定义数据的index
        int i = indexFor(hash, table.length);
        //循环该链表是否存在已经存在元素,存在则替换并返回
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //创建entry节点
        addEntry(hash, key, value, i);
        return null;
    }

void addEntry(int hash, K key, V value, int bucketIndex) {
        //是否需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //不管数组头部是否存在值都作为next节点插入(也就是上文说的头插法)
        createEntry(hash, key, value, bucketIndex);
    }

//这个是jdk1.7拉链法的实现,每次都是在头部插入
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }


//自定义EnMyHashMapEntry[] mytable = new MyHashMapEntry[10];

        /**
         *   MyHashMapEntry(int h, K k, V v, MyHashMapEntry n) {
             value = v;
             next = n;
             key = k;
             hash = h;
         }
         */
        /**
         *  Entry e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
         */

        //自己模拟hash碰撞-拉链法-头插法
        MyHashMapEntry e = mytable[0];
        mytable[0] = new MyHashMapEntry<>(1, "key-0", "value-0", e);

        MyHashMapEntry e1 = mytable[0];

        mytable[0] = new MyHashMapEntry<>(1, "key-1", "value-1", e1);

        //遍历-验证头插法
        for (MyHashMapEntry fe = mytable[0]; fe != null; fe = fe.next) {

            System.out.println("fe-key:"+fe.key+"fe-value:"+fe.value);

        }
        
```自定义entry  帮助理解

```说明:其实entry就是控制一个next 节点引用实现的链表,每次新建一个节点都会获取之前链表的头结点引用并作为自己的next引用也就是上文所说的头插法

MyHashMapEntry[] mytable = new MyHashMapEntry[10];

        /**
         *   MyHashMapEntry(int h, K k, V v, MyHashMapEntry n) {
             value = v;
             next = n;
             key = k;
             hash = h;
         }
         */
        /**
         *  Entry e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
         */

        //自己模拟hash碰撞-拉链法-头插法
        MyHashMapEntry e = mytable[0];
        mytable[0] = new MyHashMapEntry<>(1, "key-0", "value-0", e);

        MyHashMapEntry e1 = mytable[0];

        mytable[0] = new MyHashMapEntry<>(1, "key-1", "value-1", e1);

        //遍历-验证头插法
        for (MyHashMapEntry fe = mytable[0]; fe != null; fe = fe.next) {

            System.out.println("fe-key:"+fe.key+"fe-value:"+fe.value);

        }
 

死链源码分析:


void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //多线程数据丢失的原因,会有覆盖的情况
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
 
比如链表固定为 k1 ->k0 -> null
经过以下代码遍历假设原链表经过rehash都遍历在同一桶下面那此代码就实现了链表翻转。
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                //保存next节点
                Entry<K,V> next = e.next;
                //重新hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //根据hash & (length-1)  判断元素hash桶的位置也就是数组索引的位置
                int i = indexFor(e.hash, newCapacity);
                //获取数据索引位置的链表头结点设置为自己的next结点
                e.next = newTable[i];
                //把自己在赋值为数据索引位置
                newTable[i] = e;
                e = next;
            }
        }
    }
JDK1.8
源码分析
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素
    else {
        Node<K,V> e; K k;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // hash值不相等,即key不相等;为红黑树结点
        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;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) { 
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
} 

以上源码分析引用:https://snailclimb.gitee.io/javaguide/#/docs/java/collection/HashMap
threshold

threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。

MAXIMUM_CAPACITY

static final int MAXIMUM_CAPACITY = 1 << 30;

DEFAULT_INITIAL_CAPACITY

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

resize源码分析

final Node<K,V>[] resize() {

        Node<K,V>[] oldTab = table;
        //数据长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //
        int oldThr = threshold;
        
        int newCap, newThr = 0;
        //数据长度大于零
        if (oldCap > 0) {
            //如果大于这个阈值后续就随便碰撞吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //(newCap = oldCap << 1) < MAXIMUM_CAPACITY   数组长度扩容一倍是否小于最大配置项
            //&& oldCap >=16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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;
    }
// An highlighted block
Integer  foo = 1 ;

关于一些细节的补充:

设置容量大小的是否为什么要求是2 的 N 次幂

因为在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。

只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。

这是为了实现均匀分布。

ArrayList

LinkedList

线程池 ThreadPoolExecutor

并发 synchronized 关键字

并发 volatile 关键字

并发组件 ThreadLocal

并发组件 AbstractQueuedSynchronizer

你可能感兴趣的:(技术积累)