本文探究的问题
-
从ArrayList的构造函数分析数据结构。
-
底层动态扩容的过程和加载因子。
-
ArrayList有哪些线程安全问题?有哪些手段来规避线程安全问题?
提醒:各位看官姥爷,注意看我代码里面的注释哈。
ArrayList的初始化
1.无参构造 - ArrayList()
ArrayList arrayList = new ArrayList();
//先上结论:在无给定大小的情况下初始化数组的长度是 0 ;在第一次添加的时候才会给定 10 的长度
transient Object[] elementData; //ArrayList存放数据的数组 空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; //集合初始化一个空数组
}
简单一看我们就得到这样的信息:构造函数有两个变量elementData、DEFAULTCAPACITY_EMPTY_ELEMENTDATA。而且ArrayList的底层是由数组实现的,且我们没有给初始长度的时候默认给的是一个空数组;
[问题:空的怎么存数据??试试给个初始大小]
2.有参构造 - ArrayList(1)
ArrayList arrayList = new ArrayList(1);
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];//直接创建给定长度的数组
} else if (initialCapacity == 0) {//如果是 0的话
this.elementData = EMPTY_ELEMENTDATA; //还是个空数组
} else {//其他情况的话 就是非法参数
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}
源码中看到还是能传0的啊?那么我们add()加个数据试试,看看什么情况!
//新增数据
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!! ??? 这干嘛的???
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//这里会有判断的啊,如果是空的就从 DEFAULT_CAPACITY, minCapacity 选个大的。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//private static final int DEFAULT_CAPACITY = 10; 这里才给个大小为10的啊。
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
关于初始数组为0,为啥也能添加的数据的过程已经get到了,下一个构造函数看看。
3.集合类参数 - ArrayList(Collection<? extends E> c)
参数说明:
Collection:集合的子类可以当做参数
.<? extends E > :泛型,必须是E的子类
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();//将参数c(c是个数组)转换成数组
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)??这什么意思??
//c.toArray() 有坑??
if (elementData.getClass() != Object[].class)
//当真的出现 c.toArray() ,jdk 也做了处理方案。拷贝复制!!
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA; //这不就是相当于无参构造了嘛,弄了个寂寞
}
}
小小脑袋,大大的问号。不禁感慨自己太菜!!!
c.toArray()会有什么坑????[谁能告诉我?]
elementData.getClass()与Object[].class ,class 比较又是个什么玩意?(* ̄︶ ̄)
搁置疑问,接着看扩容吧... ...
动态扩容的过程
要想动态的扩容,肯定是在添加数据的时候。集合自己判断要不要扩容,去看看add咋搞的。
扩容吧小宝贝 - add(E e)
ensureCapacityInternal(size + 1) 似曾相识啊,初始化 为0的时候为啥能添加数据,刚刚看过。
private int size;//记录集合的实际存放的元素个数;
public boolean add(E e) {
//size+1 的目的:因为是添加元素,所以就要把当前数组的实际长度增加1
//看看是否超出了集合的当前容量
ensureCapacityInternal(size + 1); // Increments modCount!!
//当前存放数组元素个数+1
elementData[size++] = e;
return true;
}
来再看下 ensureCapacityInternal(int minCapacity)
private static final int DEFAULT_CAPACITY = 10;//默认容量10;
private void ensureCapacityInternal(int minCapacity) {
//如果当前数组是一个空数组的话,就取两个数的最大值。这里取得是容量10,当第一次使用add方法
//才去给数组一个固定的容量!可能是为了节约内存吧。脑壳疼。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//咳咳,去下面看这个方法的讲解
ensureExplicitCapacity(minCapacity);
}
老母猪戴胸罩,一套又一套 。来看这个tao~ ensureExplicitCapacity(minCapacity);
//来了,宝?
private void ensureExplicitCapacity(int minCapacity) {
//这个东西是记录,修改次数的。咱们这次就不管这个东西了
modCount++;
//不是从一开始就传进来一个size+1么?到这里才用上,到底扩不扩容就看这次的比较了
//当前实际大小+1之后大于了数组定义的长度,那肯定不行啊。妥妥的扩容
if (minCapacity - elementData.length > 0)
//下面去看扩容解释
grow(minCapacity);
}
扩容吧!小宝贝~
//ლ(°◕‵ƹ′◕ლ)
private void grow(int minCapacity) {
//咱们先把原数组的容量存一下
int oldCapacity = elementData.length;
//这里看出来是扩容多少倍没?
// 新容量 = 原容量 + (原容量/2^1)
int newCapacity = oldCapacity + (oldCapacity >> 1);
//这里判断这个为什么?假设初始容量设置为1,,扩容后还是1!所以需要这个判断
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//超出宇宙限制了,宇宙装不下你了,就给你整个宇宙吧。
//赋值最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//划重点,划重点。
//解释:把原来的数组elementData 拷贝一份 给elementData,且容量变为newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);//可真是一套一套又一套
}
Arrays.copyOf (elementData, newCapacity)看着有点眼熟啊!在 3.集合类参数 - ArrayList(Collection<? extends E> c) 中有个他兄弟 Arrays.copyOf(elementData, size, Object[].class); 忘了嘛?[赶紧关注点赞~]
public static <T> T[] copyOf(T[] original, int newLength) {
//真是个不干活的家伙,我们接着tao~
return (T[]) copyOf(original, newLength, original.getClass());//前后呼应,nice~
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
//源码里面也用这个啊,啧啧啧啧~
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
//上面那一坨坨不想讲了,累了
//看看这个终极boss 吧。
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//芭比Q了,native了。没得看了
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
没关系。我已经从别人家的博客文章学会了。我可以的
怎么来对应参数含义呢?(按顺序解释)
-
src:被复制的原数组
-
srcPos:开始复制的下标
-
dest:接收的数组
-
destPos:从哪个位置开始接收数据
-
length:复制长度是多少
莫着急,下面用图说话
进化吧小宝贝 - add(int index, E element)
这个是按照下标位置进行插入的,别急看完这个你就不用芭比Q了。
public void add(int index, E element) {
//是否存在index下标
rangeCheckForAdd(index);
//这个不说了,看上面的add(E e)讲解
ensureCapacityInternal(size + 1); // Increments modCount!!
//这里就是扩容的原理,在Arrays.copyOf()就是调用的这个方法
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//下标判断
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//又来到这里
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
假设原数组 elementData[] = {"a","b","c","d",""}(已经扩容了,最后一位为空),现在想在下标为1的位置插入“w”。最终为elementData[] = {"a","w","b","c","d"}
//我们再来解释下参数的含义
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData:被复制的原数组
index:开始复制的下标(从这里开始复制,我例子中的"b"元素开始复制)
elementData:接收的数组
index + 1:从哪个位置开始接收数据(我例子中的"c"元素开始复制)
size - index:复制长度是多少
上面数组复制结束后应该是 elementData[] = {"a","b","b","c","d"}
然后elementData[index] = element;
最终elementData[] = {"a","w","b","c","d"}
至于这个过程,已经不需要java代码来控制了。明白这个东西就好了。
扩容后的长度为啥是1.5倍?
我不明白,我解释不来~
因为1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。 抄的(^▽^)
int newCapacity = oldCapacity + (oldCapacity >> 1);
看看别人咋解释的。
线程不安全问题
由于jdk并没有对ArrayList的方法、成员变量,加上synchronized 或其他线程安全的处理,导致ArrayList是线程不安全的类
异常模拟代码,如果出不来。再多开个线程。
public static void main(String[] args) throws InterruptedException {
final List<Integer> list = new ArrayList<Integer>();
new Thread(()->{
for (int i = 1; i < 1000; i++) {
list.add(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 1; i < 1000; i++) {
list.add(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 1; i < 1000; i++) {
list.add(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000);
// 打印所有结果
for (int i = 0; i < list.size(); i++) {
System.out.println("第" + (i + 1) + "个元素为:" + list.get(i));
}
}
-
下标越界
-
数据为null
- 数量不对
数组下标越界
为什么会出现数组下标越界呢?
由于ArrayList添加元素是如上面分两步进行,可以看出第一个不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。
具体逻辑如下:
- 列表大小为9,即size=9
- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
- 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
- 线程B也发现需求大小为10,也可以容纳,返回。
- 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
- 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.
元素值覆盖和为空问题
elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:
elementData[size] = e;
size = size + 1;
在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:
- 列表大小为0,即size=0
- 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
- 线程A开始将size的值增加为1
- 线程B开始将size的值增加为2
这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。
线程安全处理
1 Collections.synchronizedList
List<Object> list =Collections.synchronizedList(new ArrayList<Object>);
2 为list.add()方法加锁
synchronized(list.get()) { list.get().add(model); }
3 CopyOnWriteArrayList
使用线程安全的 CopyOnWriteArrayList 代替线程不安全的 ArrayList。
List<Object> list1 = new CopyOnWriteArrayList<Object>();
4 使用ThreadLocal
使用ThreadLocal变量确保线程封闭性(封闭线程往往是比较安全的, 但由于使用ThreadLocal封装变量,相当于把变量丢进执行线程中去,每new一个新的线程,变量也会new一次,一定程度上会造成性能[内存]损耗,但其执行完毕就销毁的机制使得ThreadLocal变成比较优化的并发解决方案)。
ThreadLocal<List<Object>> threadList = new ThreadLocal<List<Object>>() {
@Override
protected List<Object> initialValue() {
return new ArrayList<Object>();
}
;
上面涉及了并发编程,JUC下的一些类。必须开个专题来记录一下。
看到这了,点个赞吧。~