Java 容器
date: 2020-12-09 14:22:23
updated: 2020-12-12 17:47:23
Java 容器
- Collection
- List:可以重复
- CopyOnWriteArrayList
- Vector
- ArrayList
- LinkedList
- Set:不可重复
- HashSet & LinkedHashSet:HashSet无序,哈希表;LinkedHashSet 通过链表可以实现有序
- SortedSet & TreeSet:有序,红黑树
- EnumSet
- CopyOnWriteArraySet
- ConcurrentSkipListSet
- Queue:主要是针对多线程 JUC
- Deque:双端队列
- ArrayDeque 实现类
- LinkedList 实现类
- BlockingDeque 接口 JUC重点
- BlockingQueue:阻塞队列
- ArrayBlockingQueue
- PriorityBlockingQueue
- LinkedBlockingQueue
- TransferQueue & LinkedTransferQueue:有一定容量,但是一个生产者放进去一个值,如果没消费者来消费,就会一直在等待
- SynchronousQueue:容量为空,必须放进去一个值才能拿,也必须拿走了才能继续生产。和LinkedTransferQueue的区别就是SynchronousQueue没有容量
- Delayqueue
- PriorityQueue:小顶堆或大顶堆(具体是什么堆,通过自定义comparator),只拿最上面的那一个。由于底层实现是堆(即二叉树)实现的,所以并不能保证线程同步。如果要支持并发,需要用PriorityBlockingQueue。
- ConcurrentLinkedQueue
- DelayQueue
- Deque:双端队列
- List:可以重复
- Map
- HashMap & LinkedHashMap
- TreeMap
- WeakHashMap
- IdentityHashMap
- ConcurrentHashMap
- ConcurrentSkipListMap
为什么没有 ConcurrentTreeMap?因为树上锁的话一般就需要锁一整棵树,影响效率;同时对树进行修改,如果底层是红黑树,修改数据可能导致整棵树的转换。
ConcurrentHashMap 无法保证顺序,如果要高并发+顺序,可以使用 ConcurrentSkipListMap
CAS和Synchronized的效率比较:
- Synchronized的过程:无锁 -> 偏向锁 -> 自旋锁 -> 重量锁
- 如果线程不是很多,或者执行操作的时间不是很长的时候,cas可能会好一点
vector 和 arraylist 相似,都是动态数组,但是两者有不同
- vector 里面方法全是 synchronized 的
- vector 包含了很多传统的方法,这些方法不属于集合框架
ArrayList 不带锁,如果要带锁的话,Collections.SynchronizedList(list)
返回的就是加了锁的list,这个集合类方法会在所有操作上加一个 synchronized
LinkedList 不带锁,如果要带锁的话,Collections.SynchronizedList(list)
返回的就是加了锁的list
ArrayList 和 LinkedList 的区别:
- ArrayList 底层是数组实现,LinkedList 底层是双向链表实现
- ArrayList 查询快;LinkedList 增删快 => 原因是数组增删要移动数据,链表查询要移动指针
存储形式:
顺序存储 数组 查询快,增删慢
链式存储 链表 查询慢,增删快
散列存储 哈希 数组+链表 查询和增删很快
HashMap、LinkedHashMap 和 Treemap 的区别:
-
HashMap 无序;LinkedHashMap 通过链表维护Key的插入顺序;TreeMap 通过红黑树来维护Key的自然顺序或者自定义顺序,但不是插入顺序。
HashTable 不允许key或value为空;put get 方法都加上了synchronized,线程同步,性能较低。如果要用线程同步的HashMap可以用 ConcurrentHashMap
-
HashMap 底层是 数组+链表,链表长度大于8的时候,转成红黑树;LinkedHashMap 比HashMap 多维护一个双向链表,可以按照插入顺序从头部或尾部迭代;
LinkedHashMap 在HashMap 的基础上进行拓展,数组不变,链表增加了before和after,也就是将所有的kv(不论在数组的哪一个槽位)都通过前后指针连了起来。
LinkedHashMap 通过accessOrder 来决定是按照插入顺序还是访问顺序,accessOrder = true,会调用 afterNodeAccess() 方法,HashMap也调用了不过是空方法,LinkedHashMap 通过这个方法将访问的节点也就是kv放到链表的最后。另外在accessOrder = true的模式下,迭代LinkedHashMap的同时去查询数据,会导致fail-fast,因为afterNodeAccess()会修改modCount,迭代的顺序已经改变。
LinkedHashMap 重写了 containsValue(),HashMap 是双循环去遍历,而因为kv都通过双链表关联起来了,LinkedHashMap 可以通过直接通过head遍历链表;containsKey() 还是用的 HashMap 的,直接通过hashcode来遍历。 -
HashMap 和 LinkedHashMap 的操作时间复杂度在O(1),TreeMap 是红黑树,时间复杂度在O(logn)
HashSet 、TreeSet 和 LinkedHashSet 的区别:
-
HashSet 无序,基于HashMap来实现;LinkedHashSet 有序, 基于 LinkedHashMap 来实现的。
-
TreeSet 是 SortedSet 接口的唯一实现类(NavigableSet 接口继承 SortedSet 接口,TreeSet 实现 NavigableSet 接口),通过comparator方法来实现有序。在底层实现上,是通过Treemap的结构,也就是红黑树来实现存储。TreeSet 不允许存放null值。放入的对象必须实现HashCode()方法,放入的对象,是以hashcode作为标识的,而具有相同内容的string对象,hashcode是一样的。
-
Set 集合要求存储的元素比较重写hashcode和equals方法。比如HashSet的存放时,会先用hashcode来找位置,然后用equals来判断是否重复。
== 判断的是内存中的地址,equals 比较内存中存放的值是否相等
CopyOnWriteArrayList:在JUC包下面,基于写时复制,并发的读,当需要修改的(add、set、remove)时候,加锁,先复制整个容器,修改元素,将原容器的引用指向新的容器。适合读多写少。
copyonwrite的思想应该和redis在持久化实现的过程是一致的,即并发读,当需要修改的时候,只把需要修改的那部分复制出来进行修改,然后把引用的指针指向新的内容。这个类在实现copyonwrite的时候是复制整个容器。
CopyOnWriteArraySet:底层用的也是 CopyOnWriteArrayList 类,在add、remove修改的时候需要加锁,复制整个容器
Queue 接口
引发异常 | 返回特殊值 | |
---|---|---|
插入 | add(如果不能插入抛出异常) | offer(不超过容量的情况下插入,如果无法插入的话返回false)。在容量受限的队列中,offer要优于add |
移除头部元素 | remove(如果队列为空,返回NoSuchElementException异常) | poll(如果队列为空,返回false) |
返回头部数据 | element(如果队列为空,返回NoSuchElementException异常) | peek(如果队列为空,返回null) |
Deque:继承Queue接口,双端队列,允许头部或尾部进行操作
LinkedList 也实现了这个接口方法,add 是直接在链表的尾部加入,还可以从头部加入,addfirst或者offerfirst
ArrayDeque:实现Deque接口,是一个双向数组,或者叫循环数组。普通数组下标0是第一个元素,下标最大的是最后一个元素,在最后进行操作还可以,但是如果在头部进行操作,就需要移动所有的数据下标,而ArrayDeque通过循环数组就解决了这个问题。具体是通过移动head和tail的下标来实现。计算个数的时候通过位运算获得return (tail - head) & (elements.length - 1);
每当add数据的时候,都会判断head是否等于tail,如果是的话就扩容,所以head=tail只会出现在初始化的时候。
BlockingQueue:阻塞队列
注意事项:
- 不接受null元素
- 可以限定容量
- 主要用于实现 生产者消费者 队列,但它另外还支持 Collection 接口
- 线程安全
公平锁和非公平锁:公平锁即一个线程组里能保证每个线程都拿到锁,那么这个锁就是公平锁。如果保证不了每个线程都拿到锁,即存在有线程饿死,那么这个锁就是非公平锁。ReentrantLock 默认是非公平锁。
比如:t1获取锁,t2等待,t1释放,t2正准备获取t3插队,t3获取锁,t2继续等待。这就是非公平锁,可能会因为线程抢占问题而导致某个线程一直在等待。如果是公平锁的话,t3会发现等待队列有人,就不会随意抢占了。
ArrayBlockingQueue:基于数组,有界,初始化时限定容量,不会扩容。生产和消费公用一个锁,即两者不会真正并行。可以设置锁是否公平。
PriorityBlockingQueue:基于优先级(优先级的判断通过构造函数传入的Compator对象来决定),无界阻塞队列。生产和消费公用一个锁,即两者不会真正并行。不会阻塞生产者,统一使用的是offer(),由于队列是无界的,会一直放进去数据;但是消费的时候如果队列为空,会一直等待。如果生产者速度远大于消费者,某一刻会导致OOM。
LinkedBlockingQueue:基于链表,如果不指定容量,默认是Integer.MAX_VALUE,相当于无界,为了避免内存问题一般指定容量。内部有takeLock和putLock,保证了生产和消费可以并行执行,并且都是非公平锁。LinkedBlockingQueue
// Removes a node from head of queue.
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
TransferQueue & LinkedTransferQueue:无界。transfer() 方法将元素给消费者,如果没有消费者就会堵塞生产者;tryTransfer() 将元素立刻给一个消费者,如果没有消费者就返回false,不会将元素放入队列,还可以指定等待时间。LinkedTransferQueue
SynchronousQueue:容量为空,必须放进去一个值才能拿,也必须拿走了才能继续生产。和LinkedTransferQueue的区别就是SynchronousQueue没有容量
DelayQueue:无界。元素只有当指定的延迟时间到了,才能从里面获取到元素。即不会堵塞生产者,但是会堵塞消费者。使用场景比如用来管理一个超时未响应的连接队列。
ConcurrentLinkedQueue:通过CAS来实现