首先,ArrayList不是线程安全的,多个线程中填工时操作一个ArrayList对象,则会出现不确定的结果。
本文主要包括如下几个部分:
- 为什么ArrayList是线程非安全的?
- 替代方案(Vector类 / Colletions封装 / JUC类)
ArrayList是线程非安全详解
先看如下示例,通过ArrayList的add方法来分析ArrayList的线程安全问题。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
41import java.util.ArrayList;
import java.util.List;
/**
* ArrayList的非线程安全演示.
*
* */
class UnsafeArrayListThread extends Thread{
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
UnsafeArrayList.arrayList.add(Thread.currentThread().getName()+" "+System.currentTimeMillis());
}
}
public class UnsafeArrayList {
public static List arrayList = new ArrayList();
public static void main(String[] args) {
Thread []threadArray = new Thread[1000];
for(int i=0;i<threadArray.length;i++){
threadArray[i] = new UnsafeArrayListThread();
threadArray[i].start();
}
for(int i=0;i<threadArray.length;i++){
try {
threadArray[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int i=0;i<arrayList.size();i++){
System.out.println(arrayList.get(i));
}
}
}
在输出时,我们会遇到这样的几种情况:
- 输出值为null;
- 数组越界异常;
- 某些线程没有输出值;
1 | 1. 输出值为null; |
这些都是在多线程中使用ArrayList类所会遇到的问题.下面我们分别根据上面进行一一解答:
我们看下ArrayList的源码: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 public boolean add(E e) {
// 确保ArrayList的长度足够
ensureCapacityInternal(size + 1); // Increments modCount!!
// ArrayList加入
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 如果超过界限 数组长度增长
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
在上述过程中,会出问题的部分在于: 1. 增加元素 2. 扩充数组长度;
增加元素过程中较为容易出现问题的部分在于elementData[size++] = e;.赋值的过程可以分为两个步骤elementData[size] = e;size++;
我们分别使用两个线程来模拟插入过程.例如有两个线程,分别加入数字1与2。
运行的过程如下所示:
- 线程1 赋值 element[1] = 1; 随后因为时间片用完而中断;
- 线程2 赋值 element[1] = 2; 随后因为时间片用完中断;
- 此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了.
- 线程1 自增 size++; (size=2)
- 线程2 自增 size++; (size=3)
- 此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了.指针index指向了3.所以,导致了某些情况下值为null的情况.
数组越界情况. 我们将上方的线程运行图更新下进行演示:
前提条件: 当前size=2 数组长度为2.
- 线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
- 线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
- 线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
- 线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了.
- 由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现.
所以, ArrayList类是非线程安全的类!
解决措施
使用Vector类进行替换
将上文的public static List arrayList = new ArrayList();替换为public static List arrayList = new Vector<>();
原理:使用了synchronized关键字进行了加锁处理。1
2
3
4
5
6public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
使用Collections.synchronizedList(List)进行替换
public static List arrayList = Collections.synchronizedList(new ArrayList());
原理: 使用mutex对象进行维护处理.Object mutex = new Object(). 这边就是创建了一个临时空间用于辅助独占的处理。1
2
3
4
5
6
7
8
9
10
11
12
13
14# 转化方法如下
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
# SynchronizedList类如下
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
// 其中的add方法如下
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
}
使用JUC中的CopyOnWriteArrayList类进行替换
后记
上集合类与线程安全性的比较图。