一,Java集合
Java的集合类只要又两个接口派生而出:Collection和Tree,Collection和Tree是Java集合框架的根接口,这两个接口又包含了一些子接口的实现类。
Collection接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。
Iterator接口也是Java集合接口框架的成员,但它与Collection系列、Tree系列的集合不一样:Collection系列集合、Tree系列集合只要用于盛装其他对象,而Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也称为迭代器。
二,各Set实现类的性能分析
HashSet 和 TreeSet 是Set的两个典型实现,到底如何选择HashSet和TreeSet呢?HashSet 的性能总是比TreeSet好(特别是最常用的添加,查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。
HashSet还有一个子类:linkedHashSet,对于普通的插入、删除操作,linkedHashSet比HashSet要略微慢一点,这时由于维护链表所带来的额外开销造成的,但由于有了链表,遍历linkedHashSet会更快。
HashSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。
必须指出的是,Set的三个实现类HashSet、TreeSet和HashSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过collection工具类的synchronizedSortedSet方法来包装该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。例如
SortedSet Set = Collections.synchronizedNavigableSet(new TreeSet<>());
三,各种线性表的性能分析
ArrayList和vector是List类的两个典型实现,都是基于数据实现的List类。
vector类是一个古老的集合,具有很多缺点,通常尽量少用vector实现类。
ArrayList和vector的显著区别是:ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证集合的同步性,但vector集合则是线程安全的,无须保证该集合的同步性。因为vector是线程安全的,所以vector的性能比ArrayList的性能要低。
PriorityQueue是一个比较标准的队列实现类,而不是绝对标准的队列实现,因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序的。
Java提供的list就是一个线性表接口,而ArrayList、linkedlist又是线性表的两种典型实现,基于数组的线性表和基于链表的线性表。queue代表了队列,deque代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。
一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好,而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性能比linkedlist的性能要好,因此大部分时候都应该考虑使用ArrayList。
关于使用list集合的建议:
1,如果需要遍历list集合,对于ArrayList、vector集合,应该使用随机访问法(get)来遍历集合元素,这样性能更好,对于linkedlist集合,则应该采用迭代器(iterator)来遍历集合元素。
2,如果需要经常执行插入、删除操作来改变包含大量数据的list集合的大小,可考虑使用linkedlist集合。使用ArrayList、vector集合可能需要经常分配内部数组的大小,效果可能比较差。
3,如果多个线程需要同时访问list集合的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。
四,各Tree实现类的性能分析
从Java源码来看,Java是先实现了Tree,然后通过包装一个所有value都为null的Tree就实现了Set集合。
Properties类是Hashtable类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便。
WeekHashTree与HashTree的用法基本类似。与HashTree的区别在于,HashTree的key保留了对实际对象的强引用,这意味着只要该HashTree对象不被销毁,该HashTree的所有key所引用的对象就不会被垃圾回收,HashTree也不会自动删除这些key所对应的kev-value对,但WeekHashTree的key只保留了对实际对象的弱引用,这意味着如果WeekHashTree对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,WeekHashTree也可能自动删除这些key所对应的key-value对。
HashTree实现类是一个与枚举一起使用的Tree实现,HashTree中的所有key都必须是单个枚举类的枚举值。
对于Tree的常用实现类而言,虽然HashTree与Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashTree通常比Hashtable要快。
treeTree通常比HashTree、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为treeTree底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。
使用treeTree有一个好处:treeTree中的key-value对总是处于有序状态,无须专门进行排序操作。当treeTree被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。
对于一般的应用场景,程序应该多考虑使用HashTree,因为HashTree正是为快速查询设计的(HashTree底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Tree时,则可以考虑使用treeTree。
linkedHashTree比HashTree慢一点,因为它需要维护链表来保持Tree中key-value的添加顺序。IdentityHashTree性能没有特别出色之处,因为它采用与HashTree基本相似的实现,只是它使用==而不是equals()方法来判断元素相等。HashTree的性能最好,但它只能使用同一个枚举类的枚举值作为key。
五,collections工具类同步控制
collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
Java中常用的集合框架中的实现类HashSet、treeSet、ArrayList、ArrayDeque、linkedlist、HashTree和treeTree都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全问题。collections提供了多个类方法可以把它们包装成线程安全的集合。
public class SynchronizedTest
{
public static void main(String[] args)
{
// 下面程序创建了四个线程安全的集合对象
Collection c = Collections
.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Tree m = Collections.synchronizedTree(new HashTree());
}
}
在上面示例程序中,直接将新创建的集合对象传给了collections的synchronizedXxx()方法,这样就可以直接获取list、Set和Map的线程安全实现版本。
六,线程安全的集合类
在java.util.concurrent包下提供了大量支持并发访问的集合接口和实现类。
这些线程安全的集合类可分为如下两类:
1,以Concurrent开头的集合类,如ConcurrentHashMap。Concurrentskiplistmap、Concurrentskiplistset、Concurrentlinkedqueue和Concurrentlinkeddeque。
2,以CopyOnWrite开头的集合类,如CopyOnWriteArrayList、CopyOnWritearrayset。
其中以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。
当多个线程共享一个公共结合时,ConcurrentLinkedQueue时一个恰当的选择。ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多个线程访问ConcurrentLinkedQueue集合时无须等待。
在默认情况下,ConcurrentHashMap支持16个线程并发写入,当超过16个线程并发向该map中写入数据时,可能有一些线程需要等待。实际上,程序通过设置concurrentLevel构造参数(默认值16)来支持更多的并发写入线程。
与前面介绍的HashMap和普通集合不同的是,因为Concurrentlinkedqueue和ConcurrentHashMap支持多线程并发访问,所以当使用迭代器来遍历元素时,该迭代器可能不能反映出迭代器之后所做的修改,但程序不会抛出异常。
使用非concurrent包下的collection作为集合对象时,如果该集合对象创建迭代器后集合元素发生改变,则会引发ConcurrentModificationException异常。
由于CopyOnWritearrayset的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于CopyOnWriteArrayList集合。
对于CopyOnWriteArrayList集合,正如它的名字所暗示的,它采用复制底层数组的方式来实现写操作。
当线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对CopyOnWriteArrayList集合执行写入操作时(包括调用add()、remove()、set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。
需要指出的是,由于CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快、很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,例如缓存等。
七,参考资料
《Java疯狂讲义》