一文学习Java容器和源码阅读

体系结构

体系结构图

img img

java容器包括三大类型list、set和map,根据是否是多线程每种容器实现类游客分为俩大类;java容器的遍历除了常规的遍历方式,还有迭代器Iterator。java的迭代器允许通过迭代器删除元素;遍历集合时使用迭代器可以忽略底层细节,但是要注意的,foreach和直接使用迭代器遍历都是通过迭代器遍历;

迭代器

Iterator

迭代器由非常著名的fast-fail快速失败机制就是,如果出现可能导致程序无法正常运行,应该及早抛出异常;其主要通过:modCount、expectedModCount实现,下面介绍:

迭代器接口:

/**
集合上的迭代器。 Iterator取代了 Java 集合框架中的Enumeration 。 迭代器在两个方面不同于枚举:
  1.迭代器允许调用者在具有明确定义语义的迭代期间从底层集合中删除元素。
  2.方法名称已得到改进。
*/
public interface Iterator<E> {
    //如果迭代有更多元素,则返回true 。 
    //(换句话说,如果next将返回一个元素而不是抛出异常,则返回true 。)
    boolean hasNext();

    /*
    返回迭代中的下一个元素
    返回:迭代中的下一个元素
    抛出:
        NoSuchElementException – 如果迭代没有更多元素
    */
    E next();

    /*
    1.每次调用next只能调用此方法一次。 
    2.如果在迭代正在进行时以除调用此方法以外的任何方式修改了基础集合,则迭代器的行为是未指定的。
    抛出:
    UnsupportedOperationException – 如果此迭代器不支持remove操作
    IllegalStateException – 如果next方法还没有被调用,或者在上次调用next方法之后已经调用了remove方法
    */
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    //对每个剩余元素执行给定的操作,直到处理完所有元素或操作引发异常。 如果指定了该顺序,则操作按迭代顺序执行。 动作抛出的异常被转发给调用者。
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

众所周知,Java迭代器的使用规则是hasNext---->next---->remove[如有必要];回顾上面对于迭代器的要求:

  • 首先之所以要先hasNext再next是避免抛出异常,但在实际使用中hasNext通过并不代表不会抛出异常,众所周知迭代器生成后去添加新元素到原容器则必然会报错,按理来说删除同样会报错【但是这里有一点坑,后面重点分析】
  • 对于remove:java对于迭代器remove的要求是:只有在明确语义的情况下才能执行remove操作,即每次执行一次next才能执行一次remove
  • 通过上面可知迭代器生效期间如果修改了原集合结构,增或删再使用next时就会报错;这里就存在一个问题就是如果删除元素后没有进入next【即删除了倒数第二个元素】就不会报错,且倒数第一个元素没有被访问,但是这和迭代器原先设置完全不符;

以Vector【线程安全】源码为例子:

  • elementCount:总的数据量
  • cursor:当前访问的数据
  • modCount:结构被修改的次数即会改变容器大小的修改次数,即数据版本号;
  • expectedModCount:创建迭代器时的数据版本号
public boolean hasNext() {
			//仅仅判断是否相等,不会判断是否结构是否被修改
            return cursor != elementCount;
        }
public E next() {
            synchronized (Vector.this) {
                //判断是否被修改
                checkForComodification();
                int i = cursor;
                if (i >= elementCount)
                    throw new NoSuchElementException();
                cursor = i + 1;
                return elementData(lastRet = i);
            }
        }
public void remove() {
            if (lastRet == -1)
                throw new IllegalStateException();
            synchronized (Vector.this) {
                //同样判断是否修改,避免多线程出漏洞,(其他非线程安全的例如linkedlist同样会执行尽管还是不一定线程安全)
                checkForComodification();
                Vector.this.remove(lastRet);
                //使用迭代器删除数据后会置其版本相同
                expectedModCount = modCount;
            }
            cursor = lastRet;
            lastRet = -1;
        }

final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

ListIterator

和Iterator迭代器相似,由于是链表,所有扩展了向前的hasPrevious、previous、以及set、add

set:仅当在最后一次调用next或previous之后没有调用remove和add才能进行此调用

add新元素插入到隐式游标之前:对next的后续调用不受影响,对previous的后续调用将返回新元素. (此调用将调用nextIndex或previousIndex返回的值增加previousIndex 。)

Collection

作为list和set的容器接口,其定义了操作容器的基本方法如:size、contains、toArray、add、remove、isEmpty等;其抽象实现类是AbstractCollection,AbstractCollection提供Collection接口的骨架实现,以最大限度地减少实现此接口所需的工作。

注意区分:Collections则是集合类的一个工具类/帮助类

public interface Collection<E> extends Iterable<E> {
//主要方法
    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();

    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);
    
    void clear();

//不常用
   	default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }
    
    boolean retainAll(Collection<?> c);

    boolean equals(Object o);

    int hashCode();
    
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

 //这里是jdk1.8对于集合的新的操作方式:Stream;后面重点介绍
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

list

List基础

list:有序可重复,其实现类无论是基于链表还是数组均均满足改条件;所以:

  • 可以精确控制每个元素在列表中的插入位置
  • 用户可以通过它们的整数索引(在列表中的位置)访问元素,并在列表中搜索元素。即使是LinkedList;
  • 列表通常允许元素对e1和e2使得e1.equals(e2)

List接口

参考文档对于list有以下描述:

  • List接口提供了四种对列表元素进行位置(索引)访问的方法。 列表(如 Java 数组)是从零开始的。 请注意,对于某些实现(例如LinkedList类),这些操作的执行时间可能与索引值成正比。 因此,如果调用者不知道实现,则迭代列表中的元素通常比通过它进行索引更可取。

    ​ 最常用是:get(int where)

  • List接口提供了一个特殊的迭代器,称为ListIterator ,除了Iterator接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。 提供了一种方法来获取从列表中的指定位置开始的列表迭代器。

  • List接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。 在许多实现中,它们将执行代价高昂的线性搜索。

  • List接口提供了两种方法来有效地在列表中的任意点插入和删除多个元素

    ​ 即:remove(Object o):列表中第一次出现o、**remove(int where)**删除位置where的对象

list接口的基本实现显然是AbstractList,下面重点分析list的实现类

List基本实现类

ArrayList

Arrays

  • 此类包含用于操作数组的各种方法(例如排序和搜索)。 这个类还包含一个静态工厂,允许将数组转为列表asListf方法
  • 所有的数组都可以通过Arrays操作,包括基本数据类型,换句话说Arrays就是操作固定长度的数组的;
基础
特点
  • 底层基于数组支持容量大小动态变化允许 null 的存在
  • 同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。

核心变量

//默认初始化容量,是一个为10的常量
private static DEFAULT_CAPACITY = 10;
//用于空实例的共享空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
//也是一个空数组,跟上边的空数组不同之处在于,这个是在默认构造器时返回的。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放数组中的元素,注意此变量是transient修饰的,不参与序列化
transient Object[] elementData; 
//数组的长度,此参数是数组中实际的参数,区别于elementData.length
private int size;

重点关注为什么需要俩个空实例数组,EMPTY_ELEMENTDATA表示是记录空数组的DEFAULTCAPACITY_EMPTY_ELEMENTDATA表示是记录默认数组的的初始情况的俩者共同点是,在创建保存容器元素的数组是尽可能的加快速度同时减少内存消耗,因为static是静态变量根据对象的生命周期,所有static共享且在对象初始化前就初始化,这样就加快了使用的速度;最大的区别在于实现扩容方式不同,我们知道默认扩容方式起点扩容是10,也就是说,对于无参构造出的空实例第一次添加1个元素后就变成容器长度为10,而使用EMPTY_ELEMENTDATA【即空数组或传入0构造空实例】第一次添加一个元素容器数组长度是1

size记录数组实际保存数据量,而elementData.length得到的是真正的数组长度,不是数组元素个数!显然类似缩减容量、是否需要扩容就需要知道数组有效元素个数;

构造方法

//空构造
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//带初始容器大小构造方法
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
//构造一个包含指定集合的元素的列表,按照集合的迭代器返回的顺序
	//略
扩容和缩减容量

​ arraylist支持自动扩容并且可以通过了trimToSize手动缩减容量至当前容量

扩容机制:

显然扩容是只会在添加元素的时候才有可能出现;以最简单的add(E)为例子

public boolean add(E e) {
    //确认容量足够
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
    public void add(int index, E element) {
        //检查是否超出数组范围
        rangeCheckForAdd(index);
        //确认容量足够
       ensureCapacityInternal(size + 1);  // Increments modCount!!
        //复制后移一位
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        elementData[index] = element;
        size++;
	}

private void ensureCapacityInternal(int minCapacity) {
    //calculateCapacity--》ensureExplicitCapacity
    //calculateCapacity确认需要容量大小,本质就是检查是由size决定还是使用默认开始位置的10,只有DEFAULTCAPACITY_EMPTY_ELEMENTDATA类型才会是默认开始10;其他都是size决定
    //ensureExplicitCapacity针对需要的容量检查是否需要扩容,如有需要进行扩容===》修改数据版本modCount++
    //grow扩容:检查对当前长度1.5倍扩容后是否足够,不足则使用需求的容量来扩容,扩容后复制数组;
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //对于DEFAULTCAPACITY_EMPTY_ELEMENTDATA的默认初始检查是否直接设置初始容量为10即可
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 检查当前需要的容量是否足够,即是否需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //扩容1.5倍的由来
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //还要检查扩容1.5倍是否足够
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //新的容量是否大于最大允许容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    //复制到新的数组;Arrays.copyOf是一个系统调用,需要说明的是,该拷贝对于数组元素而言是一个浅拷贝(除非是基本数据类型及其包装类)
    elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
    //超出范围直接抛出异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //AX_ARRAY_SIZE=Integer.MAX_VALUE - 8,返回合适的大小
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

LinkedList

  • LinkedList 是一个继承于AbstractSequentialList的双向链表它也可以被当作堆栈、队列或双端队列进行操作

    ​ 这说明其有:栈:push、pop等;队列:add、peek、poll等;双端队列:addLast、peekLast、pollLast等

    双向链表:addXXX、getXXX、removeXXX、size和toArray等

  • LinkedList 实现 List 接口,能对它进行队列操作

  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用

  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆

  • LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。是非同步的。

LinkedList为了加快查找速度,通过一个计数索引值size【保存实际元素个数】首先会比较当前需要查找的位置和“双向链表长度的1/2”若前者大,则从链表头开始往后查找,直到location位置;否则,从链表末尾开始先前查找,直到location位置。这就是“双线链表和索引值联系起来”的方法。

	//元素个数,链表长度
    transient int size = 0;
	//头节点
    transient Node<E> first;
	//为节点
    transient Node<E> last;

	//构造器
    public LinkedList() {
    }
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

Vector

Vector实际就是线程安全ArrayList;不同的是其只有俩个有效构造器和扩容策略不同其允许用户自定义扩容增量或者使用默认的俩倍增量

	//元素实际保存数组
    protected Object[] elementData;
	//容器有效元素数量
    protected int elementCount;
	//增量大小,扩容的时候,如果没有指定增量大小(或者指定增量大小小于1则执行两倍扩容策略)
    protected int capacityIncrement;

//构造器
public Vector() {
        this(10);
    }

public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }


public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
    
public Vector(Collection<? extends E> c) {
        elementData = c.toArray();
        elementCount = elementData.length;
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
    }


//扩容策略
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //capacityIncrement为指定增量大小
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

List核心

Queue

Queue体系

image-20220225144445618

Queue:

  • Queue作为队列容器,一般用于操作队列两端,底层实现:主要分为数组和链表(和List类似)
  • Deque:双端优先队列
  • 作为优先队列PriorityQueue使用数组模拟堆结构实现,优先级队列的元素根据其自然顺序或在队列构建时提供的Comparator进行排序;

map

map是java容器的重点,set是基于map实现的;

Map基础

特点

  • Map集合是双列的,元素成对存储;Collection集合是单列的,元素单个存储
  • Map集合的键不能重复,Collection集合的Set分支的元素是唯一的
  • Map集合的数据结构大多数只针对键有效,与值无关;Collection集合的数据结构是针对值的

常用api

插入:put(k,v);删除:remove(k)
获取 size、get(k)、hashcode
判断:containsKey(k)、containsValue(v)、equals()、isEmpty();清除:clear()
替换(replace(k,v,v)/replacce(k,v),replace(K key, V oldValue, V newValue)

红黑树

红黑树是一个特殊的平衡二叉树;和平衡二叉树ALVTree相比其具有弱平衡的特点【就是旋转因子更大,我们知道ALV树左右子树高度相差不得超过1否则就要进行旋转】;红黑树确保没有一条路径会比其它路径长出两倍【最短的可能路径都是黑色结点,最长的可能路径有交替的红色和黑色结点。】

红黑树的规则

  1. 每个节点非红即黑
  2. 根节点是黑的;
  3. 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
  4. 如果一个节点是红的,那么它的两儿子都是黑的;【不能有两个连续的红色结点】
  5. 任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。

添加节点:

其左旋和右旋规则和ALV树一致,插入的时候可以认为是ALV树,然后将插入节点设置为红色【之所以设置为红色是为了满足条件5,否则必然不满足条件5;但是其可能违反条件4,即其父节点也为红,这时就需要进行处理】处理策略如下:

image-20211125162659652

然后对处理的新当前节点再次判断是否满足其父节点不为红色;【对于其为根节点的直接设置为黑色即可】;上面操作的核心是通过将红色节点上移使得当前节点的父节点满足条件,根节点直接变黑色即可;case2是为了变成case3处理;

删除操作比较复杂:若删除的结点是红色,则不做任何操作,红黑树的任何属性都不会被破坏;若删除的结点是黑色的,显然它所在的路径上就少一个黑色结点,红黑树的性质就被破坏了

image-20211125163902037

https://www.cnblogs.com/skywang12345/p/3245399.html

Map基本实现类

HashMap

基础
  • HashMap接口的基于哈希表,使用链地址法处理冲突【数组加链表】的实现。 此实现提供了所有可选的映射操作,并允许空值和空键。 ( HashMap类大致相当于Hashtable ,除了它是不同步的并且允许空值。
  • 不保证映射的顺序; 特别是,它不保证元素位置会随着时间的推移保持不变。
  • HashMap的实例有两个影响其性能的参数:初始容量和负载因子当哈希表中的条目数超过负载因子和当前容量的乘积时【默认负载因子 (0.75) 】重新哈希表(即重建内部数据结构),使哈希表具有大约两倍的桶数。【扩容是以约两倍扩容】;
  • 此类的所有“集合视图方法”返回的迭代器都是快速失败的【支持快速失败】
  • 我们知道Comparable不支持为空的比较的,但是红黑树需要通过比较器比较大小;换句话说Comparable.comparator方法是不能比较空值的,但是hashMap支持通过key为空的操作,这是因为其有一个通过system.identityHashCode(a)获取a的hashcode的方法,通过该方法来比较获取默认比较结果-1或1;
    • image-20211128110755575

jdk1.7对于链地址法处理冲突只是使用链表;而jdk1.8则在链表节点深度大于8时将链表变为红黑树,当要删除时如果节点个数小于6则变为链表;

问题

1.红黑树排列规则:在hashmap中使用的是:类名和hashcode来确定红黑树的顺序;

  • 另外String的hash算法如下:

  • public int hashCode() {
            int h = hash;
            if (h == 0 && value.length > 0) {
                char val[] = value;
     
                for (int i = 0; i < value.length; i++) {
                    h = 31 * h + val[i];
                }
                hash = h;
            }
            return h;
        }
    
  • 以31为权,每一位字符的ASCII只进行计算,用自然溢出来等效取模。

  • 哈希计算公式可以记为s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]。最后将hash值保持在h中,后面再次获取hashCode将直接使用;之所以使用31是因为他是一个质数

2.为什么 hashmap 用红黑树不用跳表:跳表需要额外的空间而且hashmap增上改查会更加频繁,使用红黑树会使得操作更稳定;而redis之所以使用跳表是因为方便对于范围查找;显然hashmap并不需要考虑这个;

3.为什么扩容是2的次幂?:较少碰撞,就是要尽量把数据分配均匀;最关键是可以使用^ &运算来计算hash值,计算效率高;

4.jdk1.8前为什么并发会有死循环和死链的问题?主要由于在扩容的时候其使用的是头插法,换句话说就是可能出现某一桶节点是:a–>b–>null;有俩个线程进行扩容,由于不同步,b—>a。另一个线程刚刚好将a插入并且获取到a a.next为b,即a-/>;这时第一个线程执行完毕b–>a–>null,根据java内存模型,数据从线程操作数表更新回主内存;这时原线程继续执行获取到新的b这样就会循环了;

jdk1.8后使用尾插法基本解决了这个问题;但是丢失修改、不可重复读、读脏数据并不能解决;

5.使用key的要点;【同时也是set集合的要点】;就是使用不可变的类或者可变对象作为key,或者重写equal、hashCode、比较器方法确保其比较结果是不可变;换句话说就是equal、hash和比较器比较结果应该是不可变的;否则可能放进去却取不出来;【一般用Integer、String这种不可变类当HashMap当key,而且String最为常用,因为string可以保持hashCode】

基本数据结构

hash表的节点node

node由四个属性:hash值、key、value和下一节点指针

	/**
 		节点
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        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; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

        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;
        }
    }

hashmap重要变量和常量

   	//默认初始容量,16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	//HashMap 的最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
	//默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	//HashMap 的树化阈值;
//作者认为在理想的情况下随机hashCode算法下所有节点的分布频率会遵循泊松分布(Poisson distribution) , 上面也列举了链表长度达到8的概率是0.00000006,也就是说我们几乎不可能会使用到红黑树 , 所以作者使用8作为一个分水岭
    static final int TREEIFY_THRESHOLD = 8;
	//链表化阈值,不用7是为了避免频繁树化和链表化,使用7作为缓冲
    static final int UNTREEIFY_THRESHOLD = 6;
	//扩容临界值;这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化;)该值应至少为 4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间发生冲突。
    static final int MIN_TREEIFY_CAPACITY = 64;

    /* ---------------- Fields -------------- */
	//数据节点数组
    transient Node<K,V>[] table;
	//保存缓存的 entrySet()
    transient Set<Map.Entry<K,V>> entrySet;
	//元素个数
    transient int size;
	//增删次数,数据版本用于快速失败机制
    transient int modCount;
	//要调整大小的下一个大小值(容量 * 负载因子)。如果尚未分配表数组,则该字段保存初始数组容量,
	//零表示 DEFAULT_INITIAL_CAPACITY:16
    int threshold;
	//哈希表的负载因子。
    final float loadFactor;

构造方法

//指定初始容量和负载因子    
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
        this.loadFactor = loadFactor;
    //注意这里是:返回给定目标容量的二次幂
    //其目标时找到大于或等于 initialCapacity(即cap)的最小2^N;
    //连续的异或时保证将最高位1以下的所有数字变为1;一开始cap-1是为了保证出现10000000……时第一个1后移,避免出现跳过等于的条件;最后将n+1即可得到最小的2^N或MAXIMUM_CAPACITY
    /*
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    */
        this.threshold = tableSizeFor(initialCapacity);
    }
//指定初始容量
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//空构造
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
//使用容器构造
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
put和get
>>>:无符号右移,即高位补0
>>:有符号右移,高位是什么补什么
img
get

过程

  • 首先确认hash值;hash算法:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
  • 确认hash索引,就是通过 (n - 1) & hash【等价于模运算】确认其在hash数组的位置
  • 不为空下,先检查第一个节点是否未要找的元素
  • 不是的话且有下一节点,判断其是链表还是红黑树;分别使用相应方法查找;
    public V get(Object key) {
        Node<K,V> e;
        //传入hash值和key,通过getNode获取
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    static final int hash(Object key) {
        int h;
        //注意这里h = key.hashCode()、h >>> 16、^运算;
        //这里首先算出hashCode【Integer.MAX_VALUE和0之间】,由于内存中不可能有这么多得数组,一般来说不超过2^16,又为了更加散列,使用到高位;所以使用高低16位异或【不用&、|是因为异或出现得结果是0、1出现概率都是被1/2】
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //tab、first、n;注意这里(n - 1) & hash等价于hash%n【前提是n是2^N,在map中n定义为2^N,至于为什么要这么做,普遍认为是为了这里获取的时候更加快速,同时可以保证hash桶都是等可能被使用【即散列的】】
        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);
                //否则使用链表查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
put

过程

  • 如果未初始化先初始化;检查hash桶是否已经有值,如果没有直接添加即可;
  • 如果有值,看第一个节点是否和带插入的值相等,相等则替换;
  • 查看赛是否是红黑树,是的话按会话树插入或者找到替换节点;
  • 否则遍历查看是否可以替换或者找到替换节点;
    • 这里注意如果节点深度大于7,会将链表树化或者重新hash即扩容;
    • 如果桶数小于MIN_TREEIFY_CAPACITY,进行扩容否则进行树化
  • 需要替换则替换
  • 记录数据版本和size
img
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

	/**
    hash – 密钥的散列
    key——钥匙
    value – 要放置的值
    onlyIfAbsent – 如果为真,则不更改现有值
    evict – 如果为 false,则表处于创建模式。
	*/
    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);
                        //binCount为7,即加入节点后变8,树化
                        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;
            }
        }
        //数据版本+1
        ++modCount;
        //检查是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
remove
   public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
扩容
  • hashmap扩容是以俩倍的形式扩容即(保持2N,我们知道前面初始化时就是以2N形式创建初始化容量的tableSizeFor);之所以使用俩倍的形式扩容,除了可以保持上面hash算法【异或前后16位】和求索引位置【用&代替求余】快速外,还加快了扩容后对于节点的重新hash的速度;
  • hash算法
  • 扩容后节点的重新分配,其只可能会分配到俩个地方,新数组中原索引的位置或者原长度+原索引的位置
    • //例如hashcode只假设前8位仅为0,后8位为: 1001 0100,在原容量为1000即8,在扩容2倍后为1 0000即16;其位置不需要变;但是遂于hash为1001 1100则需要变到原来的位置加所在位置

过程

  • 首先确定是否可扩容、下一扩容阈值、当前容量
  • 新hash表直接返回,扩容hash表对节点的hash位置重新确认
  • 逐一检查原hash表桶,空跳过、只有一个节点直接hash确认节点位置;否则:根据是红黑树还是链表完成重新hash
  • 重新hash就是确认位置是原位置还是hash+原容量位置;
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) {
            //超过最大值,不允许扩容,返回原hashmap
            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
        }
    	//原长度为0,阈值大于0;说明是指定容量或者同时指定负载因子和容量的构造
        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) {
            //按桶逐一hash
            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;
                            //e.hash & oldCap说明不用改变位置,继续在原位置插入即可
                            //例如hashcode只假设前8位仅为0,后8位为: 1001 0100,在原容量为1000即8,在扩容2倍后为1 0000即16;其位置不需要变;但是遂于hash为1001 1100则需要变到原来的位置加所在位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //否则说明其位置是:原来位置+oldTab;因为这是俩倍扩容
                            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;
    }

TreeMap

基础

特点

  • 非线程安全
  • treemap使用key进行排序;treemap是基于红黑树;【此实现为containsKey 、 get 、 put和remove操作提供有保证的 log(n) 时间成本
  • treepMap使用比较器进行判断大小;【comparator,所以如果要自定义比较规则必须传入,默认使用key的自然顺序;比较规则必须和equals一致;当且仅当e1.compareTo(e2) == 0 对于类C 的每个e1和e2具有与e1.equals(e2)相同的布尔值时,才说类C的自然顺序与等于一致。 请注意, null不是任何类的实例,即使e.equals(null)返回false , e.compareTo(null) 也应该抛出NullPointerException
  • 实现了NavigableMap,所以其具有导航的功能
  • TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
  • 查找速度接近二叉查找,同时支持类似
    • ceilingKey(T t): 返回大于等于t的最小的Key—》同理ceilingEntry(T t)返回的是Map.Entity<K,V>
    • floorKey:返回小于等于t的最大key—》同理floorEntry
    • higher:返回严格大于t的最小key—》higherEntry
    • lower:返回严格小于t的最大key—》lowerEntry
    • tailMap(T t, boolean about):返回所有大于等于(如果about为true)的key、value:NavigableMap

问题

基本数据结构

主要变量

/**
 * 我们前面提到TreeMap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排
 * 序,然而并不是所有情况下都可以直接使用key的自然顺序,有时候我们想让Map的自动排序按照我们自己的规则,
 * 这个时候你就需要传递Comparator的实现类
 */
private final Comparator<? super K> comparator;
 
/**
 * TreeMap的存储结构既然是红黑树,那么必然会有唯一的根节点。
 */
private transient Entry<K,V> root;
 
/**
 * Map中key-val对的数量,也即是红黑树中节点Entry的数量
 */
private transient int size = 0;
 
/**
 * 红黑树结构的调整次数,数据版本
 */
private transient int modCount = 0;

构造方法

//空比较器,按照key的自然顺序排列
public TreeMap() {
        comparator = null;
    }
//自定义比较器
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }

//传入map容器
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
//传入SortedMap
    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

节点node

 static final class Entry<K,V> implements Map.Entry<K,V> {
     //键、值对
        K key;
        V value;
     //左、右孩子
        Entry<K,V> left;
        Entry<K,V> right;
     //父节点
        Entry<K,V> parent;
     //颜色,是否为黑色
        boolean color = BLACK;
    }

最终效果img

get和put
put

过程:

img
//与key关联的先前值,如果没有key映射,则为null    
public V put(K key, V value) {
        Entry<K,V> t = root;
        //还未初始化,直接将头节点赋值即可
        if (t == null) {
            //类型(可能为空)检查
            compare(key, key); // type (and possibly null) check
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //定义一个cmp,就是确定向左还是向右继续;定义parent,是new Entry时必须要的参数
        int cmp;
        Entry<K,V> parent;

        Comparator<? super K> cpr = comparator;
        //根据有无自定义的比较器分情况进行
        if (cpr != null) {
            //这里主要是检查是否需要替换,而不是增加节点,另以分支逻辑相同
         /**
         * 从root节点开始遍历,通过二分查找逐步向下找
         * 第一次循环:从根节点开始,这个时候parent就是根节点,然后通过自定义的排序算法
         * cpr.compare(key, t.key)比较传入的key和根节点的key值,如果传入的key<root.key,那么
         * 继续在root的左子树中找,从root的左孩子节点(root.left)开始:如果传入的key>root.key,
         * 那么继续在root的右子树中找,从root的右孩子节点(root.right)开始;如果恰好key==root.key,
         * 那么直接根据root节点的value值即可。
         * 后面的循环规则一样,当遍历到的当前节点作为起始节点,逐步往下找
         *
         * 需要注意的是:这里并没有对key是否为null进行判断,建议自己的实现Comparator时应该要考虑在内
         */
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //定义新节点
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //重新红黑树化;就是实现前面介绍红黑树的保持条件4:不存在连续两个红节点
        fixAfterInsertion(e);
        //数据+1,数据版本+1;
        size++;
        modCount++;
        return null;
    }

compare:调用真正的比较方法

 final int compare(Object k1, Object k2) {
        return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
            : comparator.compare((K)k1, (K)k2);
    }
get
    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }

/**
 * 1.根据是否使用默认的比较器comparetor进行有俩个分支,默认comparetor有非空检查
 * 2.二分查找
 */
final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }


final Entry<K,V> getEntryUsingComparator(Object key) {
        @SuppressWarnings("unchecked")
            K k = (K) key;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = cpr.compare(k, p.key);
                if (cmp < 0)
                    p = p.left;
                else if (cmp > 0)
                    p = p.right;
                else
                    return p;
            }
        }
        return null;
    }
remove

remove比较复杂;先跳过

public V remove(Object key) {
    //先获取
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;
	//有值,再删除
        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }
    


private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        //如果严格为内部,则将后继元素复制到 p,然后使 p 指向后继。
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        if (replacement != null) {
            // Link replacement to parent
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            // Null out links so they are OK to use by fixAfterDeletion.
            p.left = p.right = p.parent = null;

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.
            if (p.color == BLACK)
                fixAfterDeletion(p);

            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }
比较器

comparetor必须同时有三种返回值:>0,<0,=0;

//例子
public MyComparator implements Comparator<Integer> {
        public int compare(Integer f1,Integer f2) {
            if(f1.intValue()== f2.intValue()){
                return 0;
            }
            if (f1.intValue()> f2.intValue())
            {
                return 1;
            }
            return -1;
        }
    }

ConcurrentHashMap

基础

和hashmap的区别

1、支持同步
2、不允许key、value为null
3、迭代器是弱一致性的不会报fast-fail错误

CAS

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java15开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。【一般通过同步队列+阻塞处理

​ 3.只能保证一个共享变量的原子操作;Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

jdk1.7及之前

HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占

jdk1.8前的ConcurrentHashMap可以认为是基于hashtable实现的,所以其主要思想是将hashtable分为多个小hashtable来强化并发能力;ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。其组织形式类似下图:

img

jdk1.7的ConcurrentHashMap知识点:

  • segment默认的数量为16,负载因子是0.75;node和hashmap数据结构一致

  • concurrencyLevel表示并发级别,这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数;【并发级别表示最多可以同时多少个线程操作ConcurrentHashMap】

  • ConCurrentHashMap 和 HashMap 的put()方法实现基本类似,主要了解实现并发性

    • 需要定位 2 次 (segments[i],segment中的table[i])
      由于引入segment的概念,所以需要
      1. 先通过key的 rehash值的高位segments数组大小-1与运算得到在 segments中的位置;【相当于hashmap找桶,只不过这里仅仅通过高位而不是hashcode】
      2. 然后在通过 key的rehash值table数组大小-1 相与得到在table中的位置【相当于hashmap找桶】
    • 没获取到 segment锁的线程,没有权力进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:
      1. table[i]的位置(你的值要put到哪个桶中)
      2. 通过首节点first遍历链表找有没有相同key
      3. 在进行1、2的期间还不断自旋获取锁,超过 64次 线程挂起
  • get(),和hashmap个体相似,最大不同是get到空值回去加锁

    • 由于变量 value 是由 volatile 修饰的,java内存模型中的 happen before 规则保证了 对于 volatile 修饰的变量始终是 写操作 先于 读操作 的,并且还有 volatile 的 内存可见性 保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。
    • 如果get()到的是null值才去加锁
  • 扩容机制:跟HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容,而且Segment 不扩容,扩容下面的table数组,每次都是将数组翻倍(1.new个2倍数组 2.遍历old数组节点搬去新数组

  • 计算size:

    • 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。
    • 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的 Count 的和
  • 弱一致性

    get方法和containsKey方法都是通过对链表遍历判断是否存在key相同的节点以及获得该节点的value。但由于遍历过程中其他线程可能对链表结构做了调整,因此get和containsKey返回的可能是过时的数据,这一点是ConcurrentHashMap在弱一致性上的体现。

jdk1.8后
jdk1.8后的内部类
在这里插入图片描述 image-20211127103415592

首先跳过segment,段是在jdk1.7及之前的;重点关注Node及其子类、CounterCell和Task类型子类;

各个和子类的作用和介绍:

  • Node:Node类主要用于存储具体键值对;其有四个子类;

    • TreeBin:添加元素时候的写锁的获取和释放;TreeBin是一个特殊的节点,用来指向红黑树的根节点,并不存储真实的元素,因此它的节点的哈希值是一个固定的特殊值-2,后面检查hash值就会知道节点是链表还是红黑树就是基于这里;
    • TreeNode:显然就是红黑树节点,和hashmap的一样
    • ForwardingNode和TreeBin一样,并不存储实际元素,而是指向nextTable,哈希值也是一个特殊的固定值(-1)。它在扩容中会使用,表示这个桶上的元素已经迁移到新的数组中去了。
    • ReservationNode同样是一个特殊值,在putIfAbsent时使用。因为put时需要对桶上的元素上对象锁(ConcurrentHashMap并非是完全无锁的,只是尽可能少的去使用锁),这时就会添加一个临时占位用的节点ReservationNode。
  • CounterCell类主要用于对baseCount的计数;CounterCell这个是jdk1.8的重大更新,其和原子变量都实现无锁同步操作但是,原子变量主要是通过自旋锁+CAS;但是CounterCell是基于Striped64、LongAddr【本质就是CAS+hash表】:不在类似原子变量一样只有一个桶,而是拥有多个桶,采用hash方法;具体就是:将一个long切割成多个long,base是无冲突的时候使用CAS保存数据的变量,如果CAS失败【有线程冲突】,使用hash数组保存【这时又可以分为hash数组为null,因为hash表消耗内存,只有需要的时候才会去创建hash表,容量不足就以hashmap的方式扩容;具体查看原子变量】

  • CollectionView类

    CollectionView抽象类主要定义了视图操作,其子类KeySetView、ValueSetView、EntrySetView分别表示键视图、值视图、键值对视图,拥有set的操作,对视图均可以进行操作,操作同样作用再map上。简单来说,几个view就是为了将map转发为set【这也是后面hashSet为什么底层可以是hashmap】;同时再次基础上,实现获取迭代器和使用迭代器操作;但是这里的迭代器是弱一致性的,换句话说如果再迭代器迭代的同时,通过put等方法添加数据改变数据结构并不会报错;下面会重点介绍弱一致性:

    • 最后说明和hashmap的迭代器不同,hashmap的迭代器同样是强一致性的,换句话说其迭代器报错和list是一样的。当然其他的例如获取set视图有该类;
  • BulkTask处理批量任务,众多子类,基本上包含Task的内部类就是其子类;有一系列task实现,主要是可以通过调用其执行某一个方法,更准确的说就是在某一个桶所有元素执行某一个方法,例子如下:

    • public <U> U searchKeys(long parallelismThreshold,
                                  Function<? super K, ? extends U> searchFunction) {
              if (searchFunction == null) throw new NullPointerException();
              return new SearchKeysTask<K,V,U>
                  (null, batchFor(parallelismThreshold), 0, 0, table,
                   searchFunction, new AtomicReference<U>()).invoke();
          }
      
      static final class SearchKeysTask<K,V,U>
              extends BulkTask<K,V,U> {
              final Function<? super K, ? extends U> searchFunction;
              final AtomicReference<U> result;
              SearchKeysTask
                  (BulkTask<K,V,?> p, int b, int i, int f, Node<K,V>[] t,
                   Function<? super K, ? extends U> searchFunction,//执行的方法
                   AtomicReference<U> result) {
                  super(p, b, i, f, t);
                  this.searchFunction = searchFunction; this.result = result;
              }
          ……
      }
      
加锁总结
  • 加锁的操作:treeifyBin、transfer、merge、compute、computeIfPresent、computeIfAbsent、clear、replaceNode、putVal;这些加锁操作的共同点就是:只将锁加在桶的头结点,这样就能避免对于整个对象加锁(很好的利用了concurrenthashmap1.7的分段的理念,不过这里实现更加精巧)
    • clear:清空节点(每次加锁需要清空的桶头结点的位置,最后将桶头结点也清空);
    • put调用putVal实现(如果该桶的节点不为空)添加节点时需要在该桶的头结点加锁
    • remove、replace等通过调用replaceNode实现
      • replaceNode同样是在桶的头结点加锁,如果恰好删除的位置是头结点、加锁的位置也是头结点,replaceNode使用直接在内存中修改桶数组的该位置(使得桶头结点为下一节点)
    • transfer:扩容
    • treeifyBin:树化
    • compute、computeIfPresent、computeIfAbsent
    • merge:合并
  • get并不需要加锁,这是因为:桶的节点的value、next均使用volitale,保证了线程的可见性

jdk1.8后ConcurrentHashMap可以认为是根据hashmap修改的,增加了实现的同步;这一点和jdk1.7完全不同;其同步的方式是CAS+Synchronized

jdk1.8后的数据结构
// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
private static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6
static final int UNTREEIFY_THRESHOLD = 6;
//树化最小hash桶数
static final int MIN_TREEIFY_CAPACITY = 64;
/**
				<!---下面是ConcurrentHashMap和hashmap不同的变量、常量----->
*/
//扩容线程每次最少要迁移16个hash桶
private static final int MIN_TRANSFER_STRIDE = 16;
//sizeCtl 中用于生成标记的位数。 对于 32 位数组,必须至少为 6
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1; 
// 树根节点的hash值
static final int TREEBIN   = -2; 
// ReservationNode的hash值
static final int RESERVED  = -3; 
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();

//      					<!--CAS实现需要的辅助变量-->
	//存放node的数组
//相当于redis的hash的h[0]
transient volatile Node<K,V>[] table;
	//下一个要使用的表; 仅在调整大小时非空。
//自然就是h[1],只在扩容有用
private transient volatile Node<K,V>[] nextTable;
    /*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
     *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
     *当为0时:代表当时的table还没有被初始化
     *当为正数时:表示初始化或者下一次进行扩容的大小*/
//所以这个就是加强版的threshold
private transient volatile int sizeCtl;
	//调整大小时要拆分的下一个表索引(加一)
private transient volatile int transferIndex;
	//调整大小和/或创建 CounterCell 时使用的自旋锁(通过 CAS 锁定)。
// 标识当前cell数组是否在初始化或扩容中的CAS标志位
private transient volatile int cellsBusy;

//            <!--    为了避免仅仅对于size操作就需要加锁或者自旋    --->
	//基本计数器值,主要在没有争用时使用,但也用作表初始化竞争期间的后备。 通过 CAS 更新
//当没有争用时,使用这个变量计数。
private transient volatile long baseCount;
	//计数器单元格表。 当非空时,大小是 2 的幂。
//相当于备用size数组,为了避免多线程每次修改size都要自旋,所以将size分片为CounterCell,而不用自旋等待baseCount可修改
private transient volatile CounterCell[] counterCells;
jdk1.8的put和get

和hashmap区别

  • 计算hash值使用的是spread算法,该算法增加了和0x7fffffff与操作保证其在正常节点的hash可用位;
  • 初始化和空节点均使用CAS尝试先无锁完成;其中初始化还使用自旋方式;
  • 只有需要遍历某一个桶的时候来进行put,才会加锁【这里不能用CAS主要是因为对象不能通过CAS进行,如果使用辅助变量CAS+自旋;即初始化的方式;有俩个问题:1.如果获取到才开始找,这个尽管没加锁但是并不能提升性能因为自旋本身需要消耗CPU;2.如果先提前找到,然后自旋+CAS,显然有多少节点就要有几个原子变量进行CAS防止找到该位置被修改同样不合理】
put
  • 初始化检查:key、value不为空、表已经初始化;表未初始化或者容量为0,直接初始化CAS+自旋
  • 计算hash桶位置、首先查桶是否为空,桶为空直接cas尝试插入
  • 插入失败说明桶已经有元素,如果哈希表没在扩容直接加锁遍历查找替换(插入);
public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        两次hash,减少hash冲突,可以均匀分布;第一次和hashmap算法一样就是高低16位hash,然后再和0x7fffffff与,正常节点的hash可用位
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //Node为空,初始化Node
            if (tab == null || (n = tab.length) == 0)
                //使用 sizeCtl 中记录的大小初始化表;并且sizeCtl会告诉其他线程正在初始化
                //CAS+自旋锁
                tab = initTable();
            //空节点,直接尝试CAS将节点放入;放入失败表明有并发线程已经完成此操作,需要进入下面节点的put操作
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                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)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //加锁遍历查找替换或插入新节点在桶f处
                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) {
                    //大于使用树而不是列表的 bin 计数阈值;也说明是链表,如果是树的话就是2不可能大于8
                    if (binCount >= TREEIFY_THRESHOLD)
                        //树化【可能;规则和hashmap一样,桶数量少于64扩容不树化】
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
         统计节点个数,检查是否需要resize
        addCount(1L, binCount);
        return null;
    }
//协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
    //检查节点f是否再扩容:ForwardingNode,f没在直接返回
        if (tab != null && (f instanceof ForwardingNode) &&
            //这一个很重要就是扩容中的节点
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            //查看用于调整大小为 n 的表的标记位。
            int rs = resizeStamp(tab.length);
            //显然每次扩容一定的数目16
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                //本线程确认扩容数目和位置
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    //扩容
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }
  • initTable
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
    //自旋的循环节点
        while ((tab = table) == null || tab.length == 0) {
            //检测其他线程是否正在初始化
            if ((sc = sizeCtl) < 0)
                //如果是直接表示让出处理器,下一次再尝试获取
                Thread.yield(); // lost initialization race; just spin
            //
            /**
	Unsafe类下的CAS实现方法,该方法是native本地方法;参数分别为:对象、偏移量、期待值、传入值;
	简单来说就是和内存的o偏移位offset开始的int数据比较是否等于expected,相等改为x并返回true;否则返回false
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
	偏移量计算同样使用Unsafe类的objectFieldOffset方法、通过传入class的getDeclaredField获取到的变量获取;
SIZECTL = U.objectFieldOffset(O。getClass().getDeclaredField("sizeCtl"));			
            */
            //CAS获取到进入初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //查看是否初始化
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //恢复sizeCtl
                    sizeCtl = sc;
                }
                //退出
                break;
            }
        }
        return tab;
    }
get
  • 由于变量 value 是由 volatile 修饰的,java内存模型中的 happen before 规则保证了 对于 volatile 修饰的变量始终是 写操作 先于 读操作 的,并且还有 volatile 的 内存可见性 保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。
  • 由于可能正在扩容,扩容的时候并不会影响本线程的读操作,【毕竟你都无锁读怎么影响呢】
public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        //和hashmap类似、先看可不可hash表是否可用,可用直接检测受个节点
        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;
            }
            //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        	//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        	//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        	//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //既不是首节点也不是ForwardingNode,即可能为链表或者只有一个元素
            //链表遍历
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
remove和replace

值得注意的是remove和replace操作都是通过replaceNode进行;remove是通过传入value为null完成,这或许也是为什么不允许设置value为null的原因吧;

通过concurrenthashmap的remove方法最终都会来到下面俩个方法然后进入replaceNode;

	//remove  
public boolean remove(Object key, Object value) {
        if (key == null)
            throw new NullPointerException();
        return value != null && replaceNode(key, null, value) != null;
    }
    public V remove(Object key) {
        return replaceNode(key, null, null);
    }

//replace
    public V replace(K key, V value) {
        if (key == null || value == null)
            throw new NullPointerException();
        return replaceNode(key, value, null);
    }
    public boolean replace(K key, V oldValue, V newValue) {
        if (key == null || oldValue == null || newValue == null)
            throw new NullPointerException();
        return replaceNode(key, newValue, oldValue) != null;
    }
final V replaceNode(Object key, V value, Object cv) {
    //计算hash值
        int hash = spread(key.hashCode());
    //自旋
        for (Node<K,V>[] tab = table;;) {
            //fh表示桶位头结点 hash
            //n表示当前table数组长度
            //i表示hash命中桶位下标
            Node<K,V> f; int n, i, fh;
            //无效直接返回
            if (tab == null || (n = tab.length) == 0 ||
                (f = tabAt(tab, i = (n - 1) & hash)) == null)
                break;
            //说明当前table正在扩容中,当前是个写操作,所以当前线程需要协助table完成扩容。
            else if ((fh = f.hash) == MOVED)
                //尝试协助扩容【在未扩容完成,且不需要协助的话就会自旋】
                tab = helpTransfer(tab, f);
            else {
                //进入这里说明其是一个未在扩容且有效的桶
                V oldVal = null;
                boolean validated = false;
                //加锁当前桶位 头结点,加锁成功之后会进入代码块。避免多线程remove
                synchronized (f) {
                    //判断sync加锁是否为当前桶位 头节点,防止其它线程,在当前线程加锁成功之前,修改过桶位的头					结点。如果修改过释放锁、自旋再次尝试
                    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;
                                    //cv为null说明就是直接操作不需要判断key-value的value是否相等
                                    //否则判断value是否相等
                                    if (cv == null || cv == ev ||
                                        (ev != null && cv.equals(ev))) {
                                        oldVal = ev;
                                        //value不为空为替换操作
                                        if (value != null)
                                            e.val = value;
                                        //pred不为空为删除非头节点
                                        else if (pred != null)
                                            pred.next = e.next;
                                        else
                                            //setTabAt起使就仅仅是调用了原子操作而已;本质还是删除;调用原子操作保证不会在删除一半的时候出问题,比如
                                            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;
                            //findTreeNode就是查看该节点
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(hash, key, null)) != null) {
                                //进入这里说明找到了
                                V pv = p.val;
                                //cv为null说明就是直接操作不需要判断key-value的value是否相等
                               //否则判断value是否相等
                                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)
                            //显然删除需要计数-1
                            addCount(-1L, -1);
                        return oldVal;
                    }
                    break;
                }
            }
        }
        return null;
    }
扩容机制

首先介绍LongAdder,我们知道原子变量是通过CAS+自旋实现的:例如AtomicLong(incrementAndGet代码如下);这样在高并发下将可能出现大量自旋,浪费CPU资源;为了较少自旋诞生了LongAdder,其思想是切片,即将long分为多个cell和base,如果遇到冲突不在只是在base上执行,而是尝试去在cell数组执行;cell本质是一个hash表,扩容策略同样使用的是hash的2倍扩容【卧槽又回到了起点】;不展开了在原子变量哪里搞吧;

public final long incrementAndGet() {
        for (;;) {
            long current = get();
            long next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

Cell

 // 为提高性能,使用注解@sun.misc.Contended,用来避免伪共享
 // 伪共享简单来说就是会破坏其它线程在缓存行中的值,导致重新从主内存读取,降低性能
 @sun.misc.Contended static final class Cell {
        //用来保存要累加的值
        volatile long value;
        Cell(long x) { value = x; }
        //使用UNSAFE类的cas来更新value值
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
        private static final sun.misc.Unsafe UNSAFE;
        //value在Cell类中存储位置的偏移量;
        private static final long valueOffset;
        //这个静态方法用于获取偏移量
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

concurrenthashmap就是参考这种思想设置的,所以其扩容策略本质就是LongAdder的扩容策略思想;

  • **首先需要计算size;**我们知道size并不是只是一个字段,其保存在CounterCell[]和baseCount中;put调用扩容机制是通过addCount,先将本次操作加上去的节点修改上述字段然后sumCount;
addCount
//参数:
    //x - 要添加的计数
    //check - 如果 <0,不检查调整大小,如果 <= 1 只检查是否无竞争    
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        // counterCells为空说明可能当前无进程在对baseCount进行修改,就利用CAS尝试更新baseCount计数
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            //as为空或者没有值,说明是尝试修改baseCount失败,而且没办法直接对counterCells进行操作;
            //ThreadLocalRandom与当前线程隔离的随机数生成器然后& m保证在m范围。如果通过一次随机获取counterCells为空不再尝试随机获取
            //如果获取到尝试CAS修改counterCell
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                //自旋+CAS执行计数
                fullAddCount(x, uncontended);
                return;
            }
            //check:<0,不检查调整大小,如果 <= 1 只检查是否无竞争
            if (check <= 1)
                return;
            s = sumCount();
        }
    //如果需要检查,检查是否需要扩容,在 putVal 方法调用时,默认就是要检查的;
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
         // 如果s 大于 sizeCtl(达到扩容阈值需要扩容) 且
        // table 不是空;且 table 的长度小于 1 << 30。(可以扩容)
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                // 如果正在扩容
                if (sc < 0) {
                // 如果 sc 的低 16 位不等于 标识符(校验异常 sizeCtl 变化了)
                // 如果 sc == 标识符 + 1 (扩容结束了,不再有线程进行扩容)(默认第一个线程设置 sc ==rs 左移 16 位 + 2,当第一个线程结束扩容了,就会将 sc 减一。这个时候,sc 就等于 rs + 1)
                // 如果 sc == 标识符 + 65535(帮助线程数已经达到最大)
                // 如果 nextTable == null(结束扩容了)
                // 如果 transferIndex <= 0 (转移状态变化了)
                // 结束循环
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 如果可以帮助扩容,那么将 sc 加 1. 表示多了一个线程在帮助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        // 扩容
                        transfer(tab, nt);
                }
                // 如果不在扩容,将 sc 更新:标识符左移 16 位 然后 + 2. 也就是变成一个负数。高 16 位是标识符,低 16 位初始是 2.就是用SIZECTL记录有一个线程在扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //扩容
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
transfer

将每个 bin 中的节点移动和/或复制到新表;

 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
     //n为旧tab的长度,stride为步长(就是每个线程迁移的节点数)
        int n = tab.length, stride;
     单核步长为1,多核为(n>>>3)/ NCPU,最小值为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
				 构建一个nextTable,大小为table两倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
     advance为true,可以继续迁移下一个节点,false则停止迁移
        boolean advance = true;
     //在提交 nextTab 之前确保扫描
        boolean finishing = false; // to ensure sweep before committing nextTab
     //CAS不断尝试分配任务、直到分配成功或者已经由其他线程完成
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //已经完成退出,并将table设置为空
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                //这里和hashmap差别不大,不过这里迁移的时候加锁
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }
弱一致性

concurrenthashmap的get方法并没有同步,其次迭代器【key、value、entity三个迭代器】在迭代的时候并不会检查数据版本或者说是否修改了concurrenthashmap结构

  • 迭代器弱一致性:

LinkedHashMap

  • HashMap和双向链表合二为一
  • 【默认】LinkedHashMap使用的是LRU算法(最近最少使用);当你插入元素时它会将节点插入双向链表的链尾,如果key重复,则也会将节点移动至链尾,当用get()方法获取value时也会将节点移动至链尾
  • 可以通过有参构造选择仅仅按插入顺序排序不作LRU算法;
  • put就是直接调用hashmap的put方法,之所以可以这么做就是因为我们在put有afterNodeAccess()进行后置处理就是链接双向链表;【确实牛逼】
  • get同样时调用是调用hashmap的get;仅仅多了后置处理,将节点移动至链尾【如果有该节点】;
  • remove()方法也是调用的HashMap的remove()方法,仅仅多了afterNodeRemoval()回调方法删除链表的节点;

这里写图片描述

API

image-20211127175652225

WeakHashMap

和hashmap基本一致,要点如下

  • 当WeakHashMap某个键不再正常使用时,会被从WeakHashMap自动删除。更精确的说,对于一个给定的键,其映射的存在并不能阻止垃圾回收器对该键的丢弃,这就使该键称为被终止的,被终止,然后被回收,这样,这就可以认为该键值对应该被WeakHashMap删除。因此,WeakHashMap使用了弱引用作为内部数据的存储方案
  • WeakHashMap 中的每个键对象间接地存储为一个弱引用的指示对象。因此,不管是在映射内还是在映射之外,只有在垃圾回收器清除某个键的弱引用之后,该键才会自动移除。
  • 除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值

HashTable

和hashmap基本一致,要点如下

  • 这个一般不用,因为ConcurrentHashMap就是其优化,主要是其在几乎所有方法加锁synchronized实现同步;
  • 线程安全,HashTable的操作几乎和HashMap一致
  • 任何非null对象都可以用作键或值
  • HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;

Map核心

set

set

特点

  • 不包含重复元素的集合。 更正式地说,集合不包含一对元素e1和e2使得e1.equals(e2) ,并且至多一个空元素
  • 没有带索引的方法,所以不能使用for循环遍历,显然就需要使用迭代器;
  • 由于继承的是Collection接口,所以其方法和list差别不是很大,当然少了一些list的方法

api

image-20211127173918644

HashSet

该类底层使用的是hashmap【或者LinkedHashMap】存储数据,因此其具有hashmap的特点

  • 线程不安全、允许一个空元素、支持快速失败、不保证映射的顺序,特别是不保证顺序随时间不变【扩容影响】;
  • 其将set的数据作为key保持在hashmap的key中,hashmap的value为一个常量Object;

变量及其构造器

    private transient HashMap<E,Object> map;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();


    public HashSet() {
        map = new HashMap<>();
    }
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

	//唯一需要注意的地方就是居然提供了LinkedHashMap,但是仅仅只能给其同包下访问;起使就是给LinkedHashSet使用的
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

其基本就是调用hashMap所以不展开

举例:

public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

public boolean add(E e) {
    //这里注意put不存在的时候返回true 否则为false
        return map.put(e, PRESENT)==null;
    }

LinkedHashSet

其继承的是hashSet,使用的父构造器显然是使用LinkedHashMap构造hashmap的构造器;所以其和linkedhashset区别不大,略;

API

image-20211128092256399

TreeSet类

he那显然其和hashset一样都是使用map存储数据,其使用的是TreeMap,所以其具有TreeMap的主要特点

  • 有序的set集合,使用TreeMap存储数据;
  • 如果没有带比较器将使用key自然排序,否则使用比较器排序;
  • 非线程安全
  • 实现NavigableSet接口,支持导航;在允许null元素的实现中,导航方法的返回值可能不明确。 但是,即使在这种情况下,也可以通过检查contains(null)来消除结果的歧义。 为了避免这样的问题,这个接口的实现,鼓励的不是允许插入null元素。
  • 支持克隆、序列化。
  • 支持快速失败机制。

变量和构造方法

private transient NavigableMap<E,Object> m;
	//很显然就是作为value
    private static final Object PRESENT = new Object();

    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
	//带比较器构造
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

    public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }

    public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

其方法不在举例

    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
        return new TreeSet<>(m.tailMap(fromElement, inclusive));
    }

API

除了一般的set的api还有就是导航接口定义的API

image-20211128093642537

Stream

https://blog.youkuaiyun.com/mu_wind/article/details/109516995

在这里插入图片描述
  • Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利
  • Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

Stream可以由数组或集合创建,对流的操作分为两种:

1.中间操作,每次返回一个新的流,可以有多个。
2.终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。

stream主要有俩种:

streamparallelStream的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求

特性:

  • stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  • stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  • stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行

常用操作

//filter:其为选择操作,是一个中间操作一般后面接一个终端操作;
	//流.filter(条件).终端操作;下面例子中 x为list的元素;例如x为一个对象而且有age属性;后面为一个简单的找第一个符合的元素
list.stream().filter(x -> x.age>1).findFirst();
//map,流.map(function).终端操作
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
					intList.stream().map(x -> x + 3).collect(Collectors.toList());
排序
            // 按工资升序排序(自然排序)
		List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
				.collect(Collectors.toList());
		// 按工资倒序排序
		List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
				.map(Person::getName).collect(Collectors.toList());


//findfirst、findlast之类的不介绍很明显

//forEach(Consumer):这个很重要,需要传入合适的操作;例如
list.stream().forEach(System.out::println);
//聚和函数,以max为例子
list.stream().max(Comparator.comparing(String::length));
	// 自然排序
list.stream().max(Integer::compareTo);
list.stream().max(Comparator.comparingInt(XXX::getSalary));
list.stream().max(new Comparator<XXX>() {
			@Override
			public int compare(XXX o1, XXX o2) {
				return o1.X-o2.X;
			}
		});
//reduce:规约,即对容器求和求积等
		// 求和方式1
		Optional<Integer> sum = list.stream().reduce((x, y) -> x + y);
		// 求和方式2
		Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
		// 求和方式3
		Integer sum3 = list.stream().reduce(0, Integer::sum);
		
		// 求乘积
		Optional<Integer> product = list.stream().reduce((x, y) -> x * y)
            

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舔猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值