常见数据结构浅析
1.ArrayList、LinkedList和CopyOnWriteArrayList
ArrayList
特点
- 线程不安全
- 底层数据结构是数组(查询快,增删慢,支持快速随机访问)
- 内存占用会存在部分浪费,末尾会预留一部分容量空间
容量
当创建一个ArrayList对象时,它会分配一定的初始容量,通常为10
private static final int DEFAULT_CAPACITY = 10;
添加元素:
1.判断需要的容量是不是大于数组长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
2.扩容 扩为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
3.复制原数据到新数组
elementData = Arrays.copyOf(elementData, newCapacity);
4.把新元素添加到数组末尾
elementData[size++] = e;
移除元素:
1.计算一个元素得位置
int numMoved = size - index - 1;
2.复制
把原数组的第index+1后面的数据复制到原数组的index位置复制长度为size - index - 1
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
3.把数组最后一个元素置空
elementData[–size] = null;
解决ArraryList线程不安全
1.vector
2.Collections.synchronizedList(new ArrayList<>());
3.CopyOnWriteArrayList
LinkedList
add
LinkedList底层采用双向链表结构:具有增删快、查询慢(与ArrayList对比)
添加元素:add 源码
void linkLast(E e) {
final Node<E> l = last;
//构建元素的指向关系
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//执行 final Node newNode = new Node<>(l, e, null);会进入如下代码
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
get
查询元素
Node<E> node(int index) {
// assert isElementIndex(index);
//判断要查的元素小于集合长度的一半 则从前往后遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//判断要查的元素大于集合长度的一半 则从后往前遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
CopyOnWriteArrayList
写时复制:
CopyOnWrite 容器即写时复制容器。往一个容器添加元素时,不直接往Object[]添加,而是先将当前容器Object[]进行copy 复制出一个新得容器,Object[] newElements,然后往新的容器newElements里添加元素,添加完元素后再将原容器的引用指向新得容器setArray(newElements)。这样做的好处是可以对CopyOnWrite容器进行并发读,而不需要加锁,因为当前容器不添加任何元素。所以CopyOnWrite是一种读写分离思想,即读和写用的不同容器
写的时候使用了ReentrantLock枷锁
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
数组array本身使用了volatile,保证多线程可见性
private transient volatile Object[] array;
2.HashSet和CopyOnWriteArraySet
HashSet线程不安全其底层就是一个HashMap<E,Object> HashSet的add方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
PRESENT是(常量):
private static final Object PRESENT = new Object();
CopyOnWriteArraySet
底层依然是CopyOnWriteArrayList,线程安全
3.HashMap与ConcurrentHashMap(jdk1.8)
hashMap | concurrentHashMap |
---|---|
线程不安全的 | 线程安全 |
数组 + 链表 + 红黑树 | 分段数组 + 链表 + 红黑树 |
高并发情况下,put、remove 成员变量时可能产生线程安全问题,需加锁 | 线程安全,因为底层代码在操作每一个Node时都会对Node加锁synchronized,保证线程安全 |
读取不加锁 | 读取数据时不加锁,高效,且因为map中的value值是添加volatile关键字修饰的,可保证读取到最新值,降低CPU负载 |
元素插入后判断数组长度是否超阈,默认阈值0.75,若超阈则进行扩容,扩容大小为原数组的2的幂次方(原数组长度往左位移1),若原数组所在内存上没有连续的可用空间,则申请新的可用连续空间,将旧数组复制到新的地址,再将旧数组置为null,等待GC回收 | 同hashMap |
hash冲突:
当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。
开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法.(链表/红黑树就是为了解决hash冲突而存在的)
HashMap即是采用了链地址法.
JDK7 使用了数组+链表的方式
JDK8 使用了数组+链表+红黑树的方式
扩容:
1.先把原数组的大小扩为原来的一倍,比如现在是16,扩容后就是32
java newCap = oldCap << 1
2.把旧数组的元素赋值给新得数组
newTab[e.hash & (newCap - 1)] = e;
触发扩容的条件:
扩容的方法叫
resize
1.进入的时候判断Node的长度为0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
2.当数组长度达到 加载因子*数字最大长度
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
threshold = newThr;
if (++size > threshold)
resize();
HashMap在put一个值的流程是什么?如何扩容?
1、通过Hash算法获得key值对应的hash值
2、根据hash值确定当前key所在node数组的索引(n - 1) & hash
,如果node[i]==null 则直接创建新数组
3、如果node[i]不是空
-----------3.1 判断 当前node的头结点的 hash和key是否都相等, 相等则需要操作的就是该node
-----------3.2 判断当前节点是否为TreeNode,对TreeNode进行操作,并返回结果e
-----------3.3 如果是链表则遍历链表,key存在则返回节点e,不存在则赋值
-----------3.4 判断节点e有没有被赋值,覆盖旧值
4、hashMap size进行加1,同时判断新size是否大于扩容阈值从而判断是否需要扩容
5、扩容阈值是旧扩容阈值的2倍。newThr = oldThr << 1;
public V put(K key, V value) {
// 1.通过Hash算法获得key值对应的hash值
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 声明Node数组tab, Node节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 对tab数组赋值为当前HashMap的table, 并判断是否为空, 或者长度为0
// 为0则进行resize()数组, 并对 n赋值为当前tab的长度
// resize() 对HashMap的table扩容, 并返回扩容后的新数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2、对 node p 进行赋值, 数组所在位置 即 node p 如果是null 则直接赋值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//3、 p 不为null, 声明 node e, key k
Node<K,V> e; K k;
// 3.1、如果hash值相等且key相等, 直接将 e 赋值为当前node的头节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 3.2、 如果是红黑树, 则对树进行操作, 返回节点e
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 3.3、 对链表进行遍历, 找到对应的节点
for (int binCount = 0; ; ++binCount) {
// 将 e 赋值为 头节点p的next, 如果下一个节点为null
if ((e = p.next) == null) {
// 对节点进行赋值
p.next = newNode(hash, key, value, null);
// 如果长度到达数转换阈值, 则需要转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果e节点的hash相等, key相等, 则 直接跳出循环 e 已经被赋值为 p.next
// 3.4、此时e节点的value没有被赋值
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;
}
}
++modCount;
// 4、 是否需要扩容 threshold = 当前容量*0.75
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容
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;
}
// 新数组长度为旧数组长度的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 5、扩容阈值是旧扩容阈值的2倍
newThr = oldThr << 1; // double threshold
}
// 旧数组不存在, 相当于首次put(K, V)时, 将数组长度置为扩容阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 旧数组不存在, new HashMap()未指定长度, 初次put(K, V), 设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新的扩容阈值是0, 则将扩容阈值设置为 新数组长度*负载因子
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"})
// 创建新数组, 长度为新长度, 即原数组长度的2倍
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将table复制为新数组
table = newTab;
if (oldTab != null) {
// 对旧数组进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 旧节点node赋值
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节点
next = e.next;
// 节点hash与旧数组长度 & 的结果来决定元素所在位置, 参考上面图示所讲
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;
// 索引j处直接赋值
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 索引 j + 老数组长度位置存放hiHead
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
4 队列
ArrayBlockingQueue:
是一个基于数组结构的有界队列按FIFO原则对元素排序
LinkedBlockingQueue:
一个基于链表的阻塞队列按FIFO原则对元素排序,吞吐量高于ArrayBlockingQueue
SynchronousQueue:
一个不存储元素得队列,每个插入操作必须等到另一个线程的调用移除,否则插入操作一直处于阻塞状态,吞吐量要高于LinkedBlockingQueue
队列常用API:
依次对应上面的每一列API
Executors.newCachedThreadPool() 底层是SynchronousQueue
其他方式创建的是LinkedBlockingQueue
5.LinkedHashMap 数据淘汰
LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序
插入顺序:
保存了键值对的插入顺序,当迭代器遍历 LinkedHashMap 时,会按照键值对的插入顺序进行。
访问顺序:
保存了键值对的访问顺序,当迭代器遍历 LinkedHashMap 时,会按照键值对最后被访问的顺序进行
手写LRU
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int capacity;
public LRUCache(int capacity) {
//如果为true,就是数据按访问顺序,最近访问的在尾部
super(capacity, 0.75f, true);
this.capacity = capacity;
}
//删除头部元素
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
//测试
public static void main(String[] args) {
LRUCache<String, String> lruCache = new LRUCache<>(3);
lruCache.put("a", "a");
lruCache.put("b", "b");
lruCache.put("c", "c");
lruCache.put("d", "d");
lruCache.forEach((k, v) -> {
System.out.println(k + "," + v);
});
}
}
6.HashMap如何降低Hash冲突概率?
计算key的hash值 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
寻址 (n - 1) & hash
① key为空则取0
② 如果key不为空 则把key的hash值 与 hash值无符号右移16位 做异或运算 得到结果 异或操作为0或1的几率为各50%,计算的值更加散列
③ 寻址的过程是 table的长度减1 再与hash进行 与运算
hashMap允许key或value为null,concurrentHashMap不允许key和value为null
7.jdk1.7 hashMap出现扩容产生死循环问题?
// 把hashMap的元素迁移到新的table
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
//判断每个下标对应存放的链表是否为空
while(null != e) {
//如果table的位置有元素,那就使用头插法,把元素放在该位置
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
HashMap扩容导致死循环的主要原因在于
扩容过程中使用头插法将oldTable中的单链表中的节点插入到newTable的单链表中, 所以newTable中的单链表会倒置oldTable中的单链表。那么在多个线程同时扩容的情况下就可能导致扩容后的HashMap中存在一个有环的单链表,从而导致后续执行get操作的时候,会触发死循环,引起CPU的100%问题
。所以一定要避免在并发环境下使用HashMap
扩容之后的newTable中的单链表形成了一个环,后续执行get操作的时候,会触发死循环,引起CPU的100%问题
HashMap死循环解决方案:
1.使用线程安全的ConcurrentHashMap替代HashMap,推荐
2.使用线程安全的容器Hashtable替代,性能低,不建议
3.使用synchronized或Lock加锁,会影响性能,不建议
8.为什么重写Equals还要重写HashCode方法?
如果两个对象的Hashcode值相等的情况下,对象的内容值不一定相等;,例如 hash碰撞问题
如果使用equals方法比较两个对象内容值相等的情况下,则两个对象的Hashcode值相等
注意:equals方法默认的情况下Object类中采用==比较对象的内存地址是否相等。
9.jdk8的concurrentHashMap底层结构实现线程安全
①
CAS
在没有hash冲突时,Node放在数组上 U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v)②
synchronized
在出现hash冲突时