学完ArrayList和LinkedList,我们接着学习Vector。学习方式还是和之前一样,先对Vector有个整体认识,然后再学习它的源码;最后再通过实例来学会使用它。
- Vector介绍
- Vector数据结构
- Vector源码解析
- Vector遍历方式
学完ArrayList和LinkedList,我们接着学习Vector。学习方式还是和之前一样,先对Vector有个整体认识,然后再学习它的源码;最后再通过实例来学会使用它。
本篇博客主要的内容是介绍ArrayList的使用和对其源码进行分析,并比较ArrayList不同迭代器之间的性能
ArrayList是一个有序集合,底层是通过数组来实现的,可以动态的改变大小,相当于动态数组。与Java中的数组相比,它的容量能动态增长。他继承于AbstractList,实现了List,RandomAccess,Cloneable,java.io.Serializable接口。
类如如下:

ArrayList继承AbstractList,实现List接口。因此就具有相关的添加、删除、修改、遍历等功能。
ArrayList实现RandmoAccess接口,即提供随机访问功能,注意这个接口内部并没有任何方法,只是起到标记的作用。
ArrayList实现Cloneable接口,即覆盖了函数clone(),能被克隆。
ArrayList实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
注意的一点是:ArrayList不是线程安全的!所以不要再并发程序中使用
下面所有源码都是基于Java8来进行的分析:按照add,remove,get,Iterator,ListIterator的顺序来进行源码分析
首先看一下一些比较重要的属性,可以方便后面理解源码:
1 | //默认初始容量 |
构造函数
1 | //设置ArrayList的初始容量,当时负数时抛出错误 |
从上面可知,提供了三个默认构造函数,分别是默认构造函数,将会初始化一个空数组,指定长度的构造函数,根据指定的长度来初始化数组,添加一个集合来初始化数组,这个会初始化一个和集合长度相等的数组,并把数据添加进去。
这里补充一点,就是上面第三个构造函数,为什么还要在判断下数组元素的类型是否是Object类型的数组,主要的原因是因为,java标准库中提供了一个Arrays.asList的函数,这个函数会返回一个list,但是没有用ArrayList来实现,而是在其内部单独实现了一个ArrayList,其内部实现将底层数组定义成了T[] ,而不是Object[],所以如果传递的是这种list创建ArrayList,就会出现类型不一致的问题,这里判断的也是为了防止这一点,详细的内容可以参考利用Jdk 6260652 Bug解析Arrays.asList(学习笔记)
这里先总结下具体的添加过程
具体源码如下
1 | public boolean add(E e) { |
这里需要注意的一点是,重新设置容量大小按照下面这个公式来设置的:新的容量=原始容量*3。并且如果数组长度超过整型的最大值,则会抛出异常。
指定下标添加元素
即add(int index, E element) ,大体上和add差不多。在指定位置添加元素,需要将原先位置的元素以及后面的元素往后順移一位,大概过程如下
1 | public void add(int index, E element) { |
具体源码如下
1 | public E remove(int index) { |
remove(object)
1 | public boolean remove(Object o) { |
这个操作就比较简单,检查index范围是否正确,然后返回指定下标的元素
1 | public E get(int index) { |
这俩个是通过内部维持一个当前操作的下标来实现的,当判断hashNext的时候判断当前下标是否超过size即可。
不过要注意的一点是,每一此next操作,都会进行如下操作
1 | final void checkForComodification() { |
会检测modCounrt和迭代器保存的modCount是否相等,如果相同,则说明有其他线程修改了集合,会抛出并发修改异常。
(01) 第一种,通过迭代器遍历。即通过Iterator去遍历。
1
2
3
4
5 >Integer value = null;
>Iterator iter = list.iterator();
>while (iter.hasNext()) {
value = (Integer)iter.next();
>}(02) 第二种,随机访问,通过索引值去遍历。
由于ArrayList实现了RandomAccess接口,它支持通过索引值去随机访问元素。
1
2
3
4
5 >Integer value = null;
>int size = list.size();
>for (int i=0; i<size; i++) {
value = (Integer)list.get(i);
>}(03) 第三种,for循环遍历。如下:
1
2
3
4 >Integer value = null;
>for (Integer integ:list) {
value = integ;
>}下面通过一个实例来演示效率问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 >package Collections.cnblog.collection.list;
>import java.util.ArrayList;
>import java.util.Iterator;
>import java.util.List;
>/**************************************
* Author : zhangke
* Date : 2018/1/18 19:37
* Desc : 测出ArrayList三种哪一种遍历最快
***************************************/
>public class StudyArrayList {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
iteratorThroughFor(list);
iteratorThroughFor2(list);
iteratorThroughRandomAccess(list);
}
public static void iteratorThroughRandomAccess(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
list.get(i);
}
endTime = System.currentTimeMillis();
System.out.println("iteractorRandomAccess interval: " + (endTime - startTime));
}
public static void iteratorThroughFor2(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (Object object : list) {
}
endTime = System.currentTimeMillis();
System.out.println("iteractorfor interval: " + (endTime - startTime));
}
public static void iteratorThroughFor(List list) {
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (Iterator iterator = list.iterator(); iterator.hasNext(); ) {
iterator.next();
}
endTime = System.currentTimeMillis();
System.out.println("iteractor interval: " + (endTime - startTime));
}
>}结果如下
1
2
3 >iteractor interval: 7
>iteractorfor2 interval: 4
>iteractorRandomAccess interval: 4我测试了几次,感觉java8里面随机访问和foreach访问比较快,不过相差也不是太大。不过数据量大的话,还是推介使用随机访问。
有俩种方法可以将list转换成数组,具体的方法如下
1 |
|
有时调用toArray()函数会抛出“java.lang.ClassCastException”异常,这时就需要调用toArray(T[] contents)来解决这中异常。
toArray() 会抛出异常是因为toArray()返回的是Object[]数组,将 Object[] 转换为其它类型(类如,将Object[]转换为的Integer [])则会抛出“java.lang.ClassCastException”异常,因为Java不支持向下转型。具体的可以参考前面ArrayList.java的源码介绍部分的toArray()。
解决该问题的办法是调用 <T> T[] toArray(T[] contents) 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 >import java.util.*;
>public class ArrayListTest {
public static void main(String[] args) {
// 创建ArrayList
ArrayList list = new ArrayList();
// 将“”
list.add("1");
list.add("2");
list.add("3");
list.add("4");
// 将下面的元素添加到第1个位置
list.add(0, "5");
// 获取第1个元素
System.out.println("the first element is: "+ list.get(0));
// 删除“3”
list.remove("3");
// 获取ArrayList的大小
System.out.println("Arraylist size=: "+ list.size());
// 判断list中是否包含"3"
System.out.println("ArrayList contains 3 is: "+ list.contains(3));
// 设置第2个元素为10
list.set(1, "10");
// 通过Iterator遍历ArrayList
for(Iterator iter = list.iterator(); iter.hasNext(); ) {
System.out.println("next is: "+ iter.next());
}
// 将ArrayList转换为数组
String[] arr = (String[])list.toArray(new String[0]);
for (String str:arr)
System.out.println("str: "+ str);
// 清空ArrayList
list.clear();
// 判断ArrayList是否为空
System.out.println("ArrayList is empty: "+ list.isEmpty());
}
>}
前面已经讲解了Arraylist,但是留下了一个知识点没有说明,Iterator的fail-fast机制。本篇博客主要讲解一下这个机制。
下面是Java官方在Arraylist的介绍中一段描述fail-fast的话
1 | if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future. |
大概的意思就是说:当通过list返回一个Iterator或者ListIterator对象之后,除了Iterator和ListIterator对象自己对集合的修改之外的其他任何修改集合的行为,都会报ConcurrentModificationException。因此在面对并发修改时,迭代器会快速而干净的失败,也就是不对List对象的状态有任何的修改,同时不会在未来某个不确定的时间产生任意的风险和不确定的行为。
用下面通俗的话来说就是:
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
1 | /** |
运行上面代码,会抛出异常:java.util.ConcurrentModificationException!即,产生fail-fast事件!
结果说明:
FastFailTest中通过 new ThreadOne().start() 和 new ThreadTwo().start() 同时启动两个线程去操作list。
ThreadOne线程:向list中依次添加0,1,2,3,4,5。每添加一个数之后,就通过printAll()遍历整个list。
ThreadTwo线程:向list中依次添加10,11,12,13,14,15。每添加一个数之后,就通过printAll()遍历整个list。
当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件。
产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。那么,ArrayList是如何抛出ConcurrentModificationException异常的呢?
在前一篇博客里面我们已经分析了ArrayList的源码。下面是摘录
1 | //实现list通用的迭代器 |
从中,我们可以发现在调用 next() 和 remove()时,都会执行 checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。
要搞明白 fail-fast机制,我们就要需要理解什么时候“modCount 不等于 expectedModCount”!从Itr类中,我们知道 expectedModCount 在创建Itr对象时,被赋值为 modCount。通过Itr,我们知道:expectedModCount不可能被修改为不等于 modCount。所以,需要考证的就是modCount何时会被修改。
上一篇博客里面分析ArrayList源码时,里面只要涉及到修改ArrayList的地点,都会对modCount加1,这些修改包括add(),remove(),clear()等等。
接下来,我们再系统的梳理一下fail-fast是怎么产生的。步骤如下:
modCount++,此时modCount变成了N+1!线程a接着遍历,当它执行到next()函数时,调用checkForComodification()比较expectedModCount和modCount的大小;而expectedModCount=N,modCount=N+1,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件。至此,我们就完全了解了fail-fast是如何产生的!
即,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
抛出这种错误的原因是因为出现并发操作,主要由以下俩种方式来解决
Collections.synchronizedList包装一下Arraylist,对每个方法都是用Synchronize进行同步CopyOnWriteList来替代ArrayList从前面可以看到,Fail-fast产生的原因是expectedModCount和modCount不相等,就会抛出异常,什么时间会修改modCount,在删除或者新增的时候会修改,如果是更新呢,根据ArrayList的源码可以看出,是不会对modCount进行修改,因此如果遍历数据的时候,只是修改数据,则不会出现Fail-fast的问题,因此会带来数据不一致的问题,但是ArrayList本身也没有保证一致性。
首先,对Collection进行说明。下面是Collection的继承关系的主要类图,(这里只列举了抽象类和接口,来说明Collection的整体结构)

Collection是一个接口,它主要的俩个分支是:List 和Set。
List和Set都是接口,他们继承于Collection。
List是有序队列,这里所说的有序队列是指,按照什么顺序添加,可以以相同的顺序取出来,List中可以有相同的元素。
Set可以和数学概念中的集合类比,Set中不允许有重复的元素。
由上面的类图可以看出,首先抽象出了一个AbstractCollection抽象类,实现了Collection接口中的大部分方法,方便代码的编写。接着AbstractList和AbstractSet继承了AbstractCollection。其中AbstraList实现了List中特有的一写方法,AbstractSet实现了对于Set来说通用的一些方法。这样做可以方便子类的编写。这很好的体现了面向对象的思想。
另外要说明的一点是,Collection继承了Iterable接口,所以每一个实现了Collection接口的类中,都可以使用迭代器遍历。List系列的集合实现了一个特有的ListIterator接口,在这个接口中增加了一些添加,删除等方法。
通过上面的介绍可以发现,Collection体系中的集合并不是特别的复杂,所以只要细心的理一下,还是很容易理解和使用的。java8新增加了default方法,还有流(Stream),我们关注的是集合的使用,因此在这里就省略这部分内容。
集合框架是Java提供的工具包,包含了一些常用的数据结构:集合、链表、队列、栈、数组、映射等,方便我们日常开发。Java集合工具包位置是java.util.*。
Java集合主要可以划分为4个部分:列表(List)、集合(Set)、映射(Map)、工具类。
下图是java集合的整体框架图:


本篇文章将要介绍的是ConcurrentHashMap,你可以将这个理解为线程安全的HashMap,但是他不是想HashTable一样对所有的方法都是用Synchronize来保证线程安全。至于是如何保证线程安全的,下文会对此进行详细的介绍,也是我们研究的主要点之一。
下文将按照下面几个方面来进行介绍
前面也介绍过,这个注解作用是注解其他注解,让注解具有继承的特性。但是继承是有条件的。
下面是摘自javaDOC上的一段话
1 | Note that this meta-annotation type has no effect if the annotated type is used to annotate anything other than a class. Note also that this meta-annotation only causes annotations to be inherited from superclasses; annotations on implemented interfaces have no effect. |
中文意思如下
1 | 请注意,如果带有继承特性的注解使用在其他元素而不是类上,则注解的继承特性是没有效果的。还要注意,这个元注解只会从父类继承注解;实现接口上的注解没有作用。 |
自定义一个带有继承特性的注解
1 | @Inherited // 可以被继承 |
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本比较高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量级锁。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了一系列的锁优化措施。本篇将最浙西优化措施进行介绍。
如果对Synchronized底层原理不清楚的可以参考Synchronize实现原理,对于Mutex Lock不太清楚的可以参考Mutex lock for Linux Thread Synchronization,同时这篇浅谈Mutex (Lock)文章详细介绍了mutex时间消耗。
优化主要分为俩大类,一类是编译优化,一类是运行时优化。