简介

        本文介绍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
...

总共有四种情况:


  1. 正常输出
  2. 输出值为null;
  3. 数组越界异常;
  4. 某些线程没有输出值;

线程不安全(原因分析)

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”:

ArrayList--线程不安全的原因/保证线程安全的方法--方案/实例_arraylist

如果四条语句按照:1,2,3,4执行,那么没有问题。

如果按照1,3,2,4来执行,就会出错。以下步骤按时间先后排序:


  1. 线程1 赋值 element[1] = "a"; 随后因为时间片用完而中断;
  2. 线程2 赋值 element[1] = "b; 随后因为时间片用完而中断;
        此处导致了之前所说的一个问题(有的线程没有输出); 因为后续的线程将前面的线程的值覆盖了。
  3. 线程1 自增 size++; (size=2)
  4. 线程2 自增 size++; (size=3)
        此处导致了某些值为null的问题。因为原来size=1, 但是因为线程1与线程2都将值赋值给了element[1],导致了element[2]内没有值,被跳过了。此时指针index指向了3,所以导致了值为null的情况。

情景2:数组越界

例如:size为2,数组长度限制为2,有两个线程,分别加入字符串“a”与字符串“b”:

ArrayList--线程不安全的原因/保证线程安全的方法--方案/实例_线程安全_02

 如果四条语句按照:1,2,3,4,5,6执行,那么没有问题。

ArrayList--线程不安全的原因/保证线程安全的方法--方案/实例_arraylist_03

前提条件: 当前size=2 数组长度限制为2。

如果按照1,3,2,4来执行,就会出错。以下步骤按时间先后排序:


  1. 语句1:线程1 判断数组是否越界。因为size=2 长度为2,没有越界,将进行赋值操作。但是因为时间片用完而中断。
  2. 语句4:线程2 判断数组是否越界。因为size=2 长度为2,没有越界,将进行赋值操作。但是因为时间片用完而中断。
  3. 语句2,3:线程1重新获取到时间片,上文判断了数组不会越界,所以进行赋值操作element[size]=“a”,并且size++
  4. 语句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();