(四)常用集合与原理

1、常用List

ArrayList:底层是数组实现 Object[],线程不安全,查询和修改⾮常快,但是增加和删除慢;查询/修改多时使用;

LinkedList: 底层是双向链表 Node<E>,线程不安全,查询和修改速度慢,但是增加和删除速度快;删除/新增多时使用;

Vector: 底层是数组实现 Object[],线程安全的,操作indexof/size/remove/add等的时候使⽤synchronized进⾏加锁;已经很少使用了;

2、线程安全List

自写list:自己写个类,继承ArrayList,每个方法都加上锁;

Collections.synchronizedList(new ArrayList<>()):几乎所有的方法都加了synchronized;读写没什么区别;

CopyOnWriteArrayList<>():执行修改操作时,先获取ReentrantLock锁,然后创建一个长度+1的新书组,将数据拷贝到新数组,加入新数据,然后将原集合指向新集合,最后释放锁;

 1 //CopyOnWriteArrayList 源代码
 2 public boolean add(E e) {
 3         final ReentrantLock lock = this.lock;
 4         lock.lock();
 5         try {
 6             Object[] elements = getArray();
 7             int len = elements.length;
 8             Object[] newElements = Arrays.copyOf(elements, len + 1);
 9             newElements[len] = e;
10             setArray(newElements);
11             return true;
12         } finally {
13             lock.unlock();
14         }
15     }

CopyOnWriteArrayList:读没有加锁,修改操作(add、set、 remove等)加锁,写代价较高,若复制大对象有可能发生Full GC;读写分离+最终一致;适合 少写多读 的场景;

Collections.synchronizedList:读写均加锁synchronized,写操作较多时使用;

两者相比,写性能好-Collections.synchronizedList,读性能好-CopyOnWriteArrayList;

3、ArrayList扩容机制

JDK7及之前,ArrayList创建时,默认大小是10,增加第11个时扩容,扩容为原来的1.5倍,类似饿汉式;

JDK8及之后,默认是null,无长度,增加第1个时初始化为10,类似懒汉式;

add时判断扩容流程:

第一次添加元素,先判断大小是否是0,如果是0,则扩容到10;

若元素个数大于其容量,则扩容为 原始⼤⼩+原始⼤⼩/2;

判断与扩容 实现逻辑:传入添加元素后的新容量值,原数组判空,新容量值一直取大(默认容量值、旧容量扩后值),创建新数组,拷贝数据,指针指向;

    //源码入口
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    //判断是否是空的
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        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);
    }

    //扩容后 = 原始⼤⼩+原始⼤⼩/2
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

add方法:先 判断与扩容,数组目前位置 +1 塞新值;

get方法:先判空,再判传入index是否超出范围,最后直接返回数据index位置数据;

indexOf方法:先判传参数是否是null,null是循环判==null,非空是循环equals判是否相同,返回index;

remove方法:先判空,再判传入index是否超出范围,最后 依次移动后边元素;

    System.arraycopy(Object src 原数组, int srcPos 起始位置, Object dest 目标数组, int destPos 目标位置,int length 拷贝长度);

    System.arraycopy(elementData, index, elementData, index+1,numMove);

4、常用map

HashMap:底层是基于数组+链表,⾮线程安全的,默认容量是16、允许有空的健和值;一般用于删除与元素定位;

Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有 null的健和值;一般不怎么用;

treeMap:使⽤存储结构是⼀个平衡⼆叉树->红⿊树,默认是生序;可以⾃定义排序规则,要实现Comparator接⼝;一般用于排序;

    按照添加顺序使⽤LinkedHashMap,按照⾃然排序使⽤TreeMap,⾃定义排序 TreeMap(Comparetor c,重写compare);

ConcurrentHashMap:也是基于数组+链表,线程安全;虽然是线程安全,但是他的效率⽐Hashtable要⾼很多;

Collections.synchronizedMap():线程安全,几乎所有的方法都加了synchronized;

hashcode:顶级类Object的⽅法,所有类都是继承Object,返回是⼀个int类型的数 根据⼀定的hash规则(存储地址,字段,⻓长度等),映射成⼀个数组,即散列值;

equals:顶级类Object的⽅法,所有的类都是继承Object,返回是⼀个boolean类型 根据⾃定义的匹配规则,⽤于匹配两个对象是否⼀样;引用类型/字段匹配等;

Set,不保存重复数据,是对对应map的封装,HashSet对应的就是HashMap,treeSet对应的就是treeMap;

//HashSet源码
public HashSet() {
    map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
     return map.put(e, PRESENT)==null;
}

 

5、map源码-HashMap与ConcurrentHashMap

 HashMap底层是 数组+链表+红⿊树 (JDK8才有红⿊树,链表长度大于8,转红黑树)

Node<K,V>[] table 数组,数组每个元素都是Node的首节点,Node实现了Map.Entry<K,V>接口,每个节点都是key-value的键值对,且每个节点都指向下一个节点;
1 static class Node<K,V> implements Map.Entry<K,V> {
2         final int hash;
3         final K key;
4         V value;
5         Node<K,V> next;
.....
transient Node<K,V>[] table;

hash碰撞:不同key计算得到的Hash值相同,hashmap是链表发,要放到同个bucket中;

  解决hash碰撞:链表法、开发地址法、再哈希法等

底层结构好处:

  链表能解决hash冲突,将hash值相同的对象存在同⼀个链表中,并放在hash值对应的数组位;

  数据较少时(少于8个),遍历线性表⽐红⿊树快;

  红⿊树能提升查找数据的速度,红⿊树是平衡⼆叉树,插⼊新数据后会通过左旋,右旋、变 ⾊等操作来保持左右平衡,解决单链表查询深度的问题;

ConcurrentHashMap,在结构上无任何区别,仅仅在方法上有区别,如取spread重哈希,加锁synchronized锁,利⽤CAS获取数据;

JDK1.7: 扩容头插法,多线程同时扩容重新塞node时,易形成环;JDK1.8:扩容尾插法;

允许null值(null对应哈希是0),Integer和String更适合做key(具有不可变性),顺序与插入顺序无关,且会随着扩容而发生变化

 

6、map的put方法

HashMap 流程:

当前数组是否为空,空则扩容;

hash值命中的数组下标,是否空,空则直接创建节点塞值;

  下标为非空,判断首节点key是否一致,一致则替换;

      否则,判树形 树形插入,链表 循环判值 替换或插入 校验转红黑树;

统一 校验并扩容;

HashMap 底层源码如下:

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                boolean evict) {
 3     HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
 4     //数组判空
 5     if ((tab = table) == null || (n = tab.length) == 0)
 6         //扩容
 7         n = (tab = resize()).length;
 8     //数组hash获取下标位是否为空
 9     if ((p = tab[i = (n - 1) & hash]) == null)
10         //空直接创建节点
11         tab[i] = newNode(hash, key, value, null);
12     else {
13         //非空,判断首节点是否key一致
14         HashMap.Node<K,V> e; K k;
15         if (p.hash == hash &&
16                 ((k = p.key) == key || (key != null && key.equals(k))))
17             e = p;
18         //是否树结构
19         else if (p instanceof HashMap.TreeNode)
20             e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
21         else {
22             //非树结构,循环 判一致 替换,null 新增 判长度转红黑树
23             for (int binCount = 0; ; ++binCount) {
24                 if ((e = p.next) == null) {
25                     p.next = newNode(hash, key, value, null);
26                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
27                         treeifyBin(tab, hash);
28                     break;
29                 }
30                 if (e.hash == hash &&
31                         ((k = e.key) == key || (key != null && key.equals(k))))
32                     break;
33                 p = e;
34             }
35         }
36         if (e != null) {
37             V oldValue = e.value;
38             if (!onlyIfAbsent || oldValue == null)
39                 e.value = value;
40             afterNodeAccess(e);
41             return oldValue;
42         }
43     }
44     //扩容
45     ++modCount;
46     if (++size > threshold)
47         resize();
48     afterNodeInsertion(evict);
49     return null;
50 }

ConcurrentHashMap:

  hashtable类所有的⽅法几乎都加锁synchronized,线程安全 ⾼并发效率低;

  JDK8前,ConcurrentHashMap使⽤锁分段技术,将数据分成⼀段段存储,每个数据段配置⼀把锁segment类,这个类继承ReentrantLock来保证线程安全 技术点:Segment+HashEntry;

  JKD8后取消Segment,底层也是使⽤Node数组+链表+红⿊树,CAS(读)+Synchronized(写) 技术点:Node+Cas+Synchronized;

spread(key.hashCode()) 重哈希,减少碰撞概率;

tabAt(i) 获取table中索引为i的Node元素;

casTabAt(i) 利⽤CAS操作获取table中索引为i的Node元素;

ConcurrentHashMap逻辑是:

取重哈希,循环表,空表初始化;

hash值命中的数组下标,是否空,空则利用cas直接创建节点塞值;

  下标为非空,判扩容 锁首节点;

      判是链表,循环链表,判key是否一致,一致则替换,null则直接插入 大于8转红黑树;

      否则,判树形 树形插入;

统一 校验并扩容;

ConcurrentHashMap源码如下:

 1 final V putVal(K key, V value, boolean onlyIfAbsent) {
 2     if (key == null || value == null) throw new NullPointerException();
 3     //取重哈希
 4     int hash = spread(key.hashCode());
 5     int binCount = 0;
 6     //直接无限循环数组
 7     for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
 8         ConcurrentHashMap.Node<K,V> f; int n, i, fh;
 9         //空数组,初始化
10         if (tab == null || (n = tab.length) == 0)
11             tab = initTable();
12         //数组hash获取下标位是否为空
13         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
14             //利用cas出入节点
15             if (casTabAt(tab, i, null, new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
16                 break;
17         }
18         //判断是否需要先扩容
19         else if ((fh = f.hash) == MOVED)
20             tab = helpTransfer(tab, f);
21         else {
22             V oldVal = null;
23             //hash冲突,加锁
24             synchronized (f) {
25                 if (tabAt(tab, i) == f) {
26                     //是链表,循环
27                     if (fh >= 0) {
28                         binCount = 1;
29                         for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
30                             K ek;
31                             if (e.hash == hash &&
32                                     ((ek = e.key) == key ||
33                                             (ek != null && key.equals(ek)))) {
34                                 oldVal = e.val;
35                                 if (!onlyIfAbsent)
36                                     e.val = value;
37                                 break;
38                             }
39                             ConcurrentHashMap.Node<K,V> pred = e;
40                             if ((e = e.next) == null) {
41                                 pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
42                                         value, null);
43                                 break;
44                             }
45                         }
46                     }
47                     //是树
48                     else if (f instanceof ConcurrentHashMap.TreeBin) {
49                         ConcurrentHashMap.Node<K,V> p;
50                         binCount = 2;
51                         if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
52                                 value)) != null) {
53                             oldVal = p.val;
54                             if (!onlyIfAbsent)
55                                 p.val = value;
56                         }
57                     }
58                 }
59             }
60             if (binCount != 0) {
61                 if (binCount >= TREEIFY_THRESHOLD)
62                     treeifyBin(tab, i);
63                 if (oldVal != null)
64                     return oldVal;
65                 break;
66             }
67         }
68     }
69     //扩容
70     addCount(1L, binCount);
71     return null;
72 }

posted on 2021-05-06 15:37  奇天异下  阅读(137)  评论(0编辑  收藏  举报

导航