简介
本文介绍Java中的ArrayList如何进行线程安全的操作、为什么ArrayList不是线程安全的、LinkedList如何进行线程安全的操作。
这几个问题也是Java后端面试中经常问到的问题。
ArrayList线程安全
线程安全的操作方法
方法 | 示例 | 原理 |
Vector | List list = new ArrayList(); 替换为List arrayList = new Vector<>(); | 使用了synchronized关键字 |
Collections .synchronizedList(list) | List<String> list = Collections .synchronizedList(new ArrayList<String>()); 操作外部list,实际上修改的是原来list的数据。 | Object mutex = new Object()。 对此对象使用synchronized |
JUC中的 CopyOnWriteArrayList | CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); 适用于读多写少的并发场景。 | Write的时候总是要Copy(将原来array复制到新的array,修改后,将引用指向新数组)。任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发。 |
线程不安全(复现)
实例
package org.example.a;
import java.util.ArrayList;
import java.util.List;
class MyThread extends Thread{
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Demo.arrayList.add(Thread.currentThread().getName() + " " + System.currentTimeMillis());
}
}
public class Demo{
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 MyThread();
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));
}
}
}
运行结果
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 49
at java.util.ArrayList.add(ArrayList.java:459)
at org.example.a.MyThread.run(Demo.java:13)
Thread-3 1590288167830
Thread-7 1590288167834
Thread-57 1590288167834
...
null
Thread-951 1590288168255
Thread-254 1590288168255
...
总共有四种情况:
- 正常输出
- 输出值为null;
- 数组越界异常;
- 某些线程没有输出值;
线程不安全(原因分析)
ArrayList源码
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. 扩充数组长度;
情景1:增加元素
增加元素过程中较为容易出现问题的地方是elementData[size++] = e;。赋值的过程可以分为两个步骤elementData[size] = e; size++;。
例如size为1,有两个线程,分别加入字符串“a”与字符串“b”:
如果四条语句按照:1,2,3,4执行,那么没有问题。
如果按照1,3,2,4来执行,就会出错。以下步骤按时间先后排序:
- 线程1 赋值 element[1] = "a"; 随后因为时间片用完而中断;
- 线程2 赋值 element[1] = "b; 随后因为时间片用完而中断;
此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了。 - 线程1 自增 size++; (size=2)
- 线程2 自增 size++; (size=3)
此处导致了某些值为null的问题。因为原来size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了。此时指针index指向了3,所以导致了值为null的情况。
情景2:数组越界
例如:size为2,数组长度限制为2,有两个线程,分别加入字符串“a”与字符串“b”:
如果四条语句按照:1,2,3,4,5,6执行,那么没有问题。
前提条件: 当前size=2 数组长度限制为2。
如果按照1,3,2,4来执行,就会出错。以下步骤按时间先后排序:
- 语句1:线程1 判断数组是否越界。因为size=2 长度为2,没有越界,将进行赋值操作。但是因为时间片用完而中断。
- 语句4:线程2 判断数组是否越界。因为size=2 长度为2,没有越界,将进行赋值操作。但是因为时间片用完而中断。
- 语句2,3:线程1重新获取到时间片,上文判断了数组不会越界,所以进行赋值操作element[size]=“a”,并且size++
- 语句5,6:线程1重新获取到时间片,上文判断了数组不会越界,所以进行赋值操作。但是此时的size=3了,再执行element[3]="b"导致了数组越界。
由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现。
LinkedList线程安全
线程安全的操作方法
方法 | 示例 | 原理 |
Collections.synchronizedList(List) | public static List linkedList = Collections.synchronizedList(new LinkedList()); | Object mutex = new Object()。对此对象使用synchronized |
JUC中的ConcurrentLinkedQueue | ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue(); |