并发编程—ArrayList线程安全问题

首先,ArrayList不是线程安全的,多个线程中填工时操作一个ArrayList对象,则会出现不确定的结果。

本文主要包括如下几个部分:

  1. 为什么ArrayList是线程非安全的?
  2. 替代方案(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
41
import 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));
}

}
}

在输出时,我们会遇到这样的几种情况:

  1. 输出值为null;
  2. 数组越界异常;
  3. 某些线程没有输出值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 输出值为null;
null
Thread-11553158258958
Thread-51553158258958
Thread-71553158258958
Thread-61553158258959

3. 某些线程没有输出值;
某些输出缺少.

2. 数组越界异常;
Exception in thread "Thread-874" java.lang.ArrayIndexOutOfBoundsException: 823
at java.util.ArrayList.add(ArrayList.java:441)
at com.yanxml.multithreading.art.collection.UnsafeArrayListThread.run(UnsafeArrayList.java:17)

这些都是在多线程中使用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. 线程1 赋值 element[1] = 1; 随后因为时间片用完而中断;
  2. 线程2 赋值 element[1] = 2; 随后因为时间片用完中断;
  • 此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了.
  1. 线程1 自增 size++; (size=2)
  2. 线程2 自增 size++; (size=3)
  • 此处导致了某些值为null的问题.因为原size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了.指针index指向了3.所以,导致了某些情况下值为null的情况.

数组越界情况. 我们将上方的线程运行图更新下进行演示:

前提条件: 当前size=2 数组长度为2.

  1. 线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
  2. 线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
  3. 线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
  4. 线程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
6
public 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类进行替换

后记

上集合类与线程安全性的比较图。

------ 本文完 ------