(四)常用集合与原理
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 }