注意:juc并发容器,以及Vector/Stack/HashTable,都只是单一操作保证线程安全,但是多线程同时复合操作是有安全性问题的。
比如:v.remove(v.size() - 1) 是复合操作,可能A线程执行了删除,B线程拿到的size已经变了,导致越界异常;for循环访问同理。
interface List
- ArrayList
- LinkedList
- Vector
- CopyOnWriteArrayList
Vector(数组,已不建议使用)[线程安全]
所有公有方法全都加了synchronized,多个线程只能进行顺序访问,即使都是查询也要排队,这就大大的降低了容器的并发能力。
ArrayList(数组)
查询效率高,增删效率低,线程不安全。随机位置插入或删除元素时,需要对数组进行复制、移动代价比较高。
- 无参构造函数创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10
- 每次扩容之后容量都会变为原来的 1.5 倍左右
- Arrays.asList()方法返回的是的Arrays内部的ArrayList,用的时候需要注意
LinkedList(双向链表)
随机访问性能则要比动态数组慢,进行节点插入、删除要高效得多。
CopyOnWriteArrayList(数组)[线程安全]
Copy-On-Write容器即写时复制的容器
1.add/remove等写方法是加锁的,而读方法是没有加锁的
2.当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
3.这样做的好处是我们可以对CopyOnWrite容器进行并发的读读和读写,当然这里读到的数据可能不是最新的。因为写时复制的思想是通过延时更新的策略来实现数据的最终一致性的,并非强一致性。
interface Map
- HashMap
- TreeMap
- HashTable
- LinkedHashMap
- ConcurrentHashMap
- ConcurrentSkipListMap
TreeMap、TreeSet、HashMap(JDK1.8之后) 底层都用到了红黑树,红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
HashMap(数组+链表/红黑树)
- JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
- JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)。
- HashMap 的存取是没有顺序的,TreeMap有序。
- capacity容量,默认16,之后每次扩充为原来的2倍,始终保持 2^n(方便通过(n - 1) & hash位运算获得下标)
- loadFactor 加载因子,默认 0.75f,太大导致查找元素效率低(碰撞多),太小导致数组的利用率低。
- threshold 扩容的阈值,等于capacity * loadFactor(比如12=16*0.75f)。扩容过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
TreeMap(红黑树,有序)
- TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器;
- 用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。
LinkedHashMap(HashMap 的子类,可实现LRUCache)
HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时第三个参数==true按照访问次序排序。
public LruCache(int maxSize) {
this.maxSize = maxSize;
//accessOrder=true基于访问顺序排序;accessOrder=false基于插入顺序排序
this.map = new LinkedHashMap<K, V>(maxSize*2, 0.75f, true);
}
再包装一下get、put,加锁、put时判断maxSize。
Hashtable(已不建议使用)[线程安全]
线程安全,每个方法加了synchronized,性能没有ConcurrentHashMap高。
ConcurrentHashMap(数组 + 链表/红黑树)[线程安全]
- JDK1.8 之前 Segment 数组 + HashEntry 数组 + 链表。分段锁控制并发,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表,Segment 的个数一但初始化就不能改变。
- JDK1.8 以后 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。 cas+部分synchronized
- put:数组为空使用cas进行initTable;tabAt位置为空节点使用cas写入;tabAt位置是FWD节点hash==-1则帮助并发扩容;否则使用synchronized加锁该位置节点进行写数据(写后大于8则转换红黑树);最后addCount判断大于阈值则扩容。
- get:tabAt根据hash查找位置;头节点key相同则返回;头节点hash<0说明在扩容或是红黑树则find查找;链表遍历查找
- 扩容/并发帮助扩容:每个线程负责一个区间,线程通过逆序遍历来迁移扩容(链表通过高低位分两个链),迁移完的bucket 会被ForwardingNode替换(fd.hash=-1);
ConcurrentSkipListMap(跳表,有序)[线程安全]
ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。
ConcurrentSkipListMap和TreeMap
第一,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。
第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。
关于跳表(Skip List),它是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。
interface Set
- HashSet:使用的HashMap
- TreeSet:使用的TreeMap
- LinkedHashSet:HashSet子类,构造HashSet时使用的LinkedHashMap
- ConcurrentHashSet:使用的ConcurrentHashMap
- CopyOnWriteArraySet:使用的CopyOnWriteArrayList
- ConcurrentSkipListSet:使用的ConcurrentSkipListMap
interface Queue/Deque/BlockingQueue/BlockingDeque
- PriorityQueue
- ArrayDeque
阻塞队列都线程安全
- ArrayBlockingQueue
- LinkedBlockingQueue
- LinkedBlockingDeque
- PriorityBlockingQueue
- SynchronousQueue
- DelayQueue
Queue
插入
- add(E e):插入元素到队尾,插入成功返回true,没有可用空间抛出异常 IllegalStateException
- offer(E e): 插入元素到队尾,插入成功返回true,否则返回false
删除
- remove():获取并移除队首的元素,该方法和poll方法的不同之处在于,如果队列为空该方法会抛出异常,而poll不会
- poll():获取并移除队首的元素,如果队列为空,返回null
获取
- element():获取队列首的元素,该方法和peek方法的不同之处在于,如果队列为空该方法会抛出异常,而peek不会
- peek():获取队列首的元素,如果队列为空,返回null
Deque
- 添加了双向插入/删除/获取的接口(addFirst/addLast/......)
- 添加了栈操作接口(push/pop)
- 其它
BlockingQueue [线程安全]
在Queue的基础上实现了阻塞等待的功能,阻塞等待就是要多线程交互,所以BlockingQueue都是线程安全的。
当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。
- put(E e):向队尾插入元素。如果队列满了,阻塞等待,直到被中断为止。
- boolean offer(E e, long timeout, TimeUnit unit):向队尾插入元素。如果队列满了,阻塞等待timeout个时长,如果到了超时时间还没有空间,抛弃该元素。
- take():获取并移除队首的元素。如果队列为空,阻塞等待,直到被中断为止。
- poll(long timeout, TimeUnit unit):获取并移除队首的元素。如果队列为空,阻塞等待timeout个时长,如果到了超时时间还没有元素,则返回null。
- remainingCapacity():返回在无阻塞的理想情况下(不存在内存或资源约束)此队列能接受的元素数量,如果该队列是无界队列,返回Integer.MAX_VALUE。
- drainTo(Collection<? super E> c):移除此队列中所有可用的元素,并将它们添加到给定 collection 中。
- drainTo(Collection<? super E> c, int maxElements):最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定 collection 中。
=========================
ArrayBlockingQueue(数组、有界阻塞队列)
- 使用循环数组进行存储(采用循环数组putIndex/takeIndex)
- 使用ReentrantLock来保证线程安全
- 由Condition的await和signal来实现等待唤醒功能
- 支持对生产者和消费者进行公平的调度(默认不公平)。公平性通常会降低吞吐量,但是减少了可变性和避免了线程饥饿问题
LinkedBlockingQueue(链表、有界阻塞队列)
- LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题
- 由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响
- LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能,而ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,
PriorityBlockingQueue(无界阻塞队列)
支持优先级排序
SynchronousQueue(不存储元素的阻塞队列)
每一个put操作必须等待一个take操作,否则不能继续添加元素
DelayQueue(无界阻塞队列)
支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
=========================
PriorityQueue(二叉堆)
线程不安全,需要线程安全使用PriorityBlockingQueue。
利用了二叉堆的数据结构来实现的,典型例题包括堆排序、求第K大的数、带权图的遍历等。