简介
本文介绍Java中的ArrayList、LinkedList如何进行线程安全的操作、为什么ArrayList不是线程安全的。
这几个问题也是Java后端面试中经常问到的问题。
线程安全的操作方法
ArrayList
方法 | 示例 | 原理 |
Vector | List list = new ArrayList(); 替换为List arrayList = new Vector<>(); | 使用了synchronized关键字 |
Collections .synchronizedList(list) | List<String> list = Collections.synchronizedList(new ArrayList<>()); 操作外部list,实际上修改的是原来list的数据。 注意:因为数据没用volatile,所以用迭代器的地方需要加锁,间接用到迭代器的地方也要加锁,比如:toString、equals、hashCode、containsAll等。 | 方法都加了synchronized修饰。加锁的对象是当前SynchronizedCollection实例。 |
JUC中的 CopyOnWriteArrayList | CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(); 适用于读多写少的并发场景。 读数据时不用加锁,因为它里边保存了数据快照。 | Write的时候总是要Copy(将原来array复制到新的array,修改后,将引用指向新数组)。任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发。 |
LinkedList
方法 | 示例 | 原理 |
Collections.synchronizedList(List) | public static List linkedList = Collections.synchronizedList(new LinkedList()); | 所有方法都加了synchronized修饰。加锁的对象是当前SynchronizedCollection实例。 |
JUC中的ConcurrentLinkedQueue | ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue(); |
线程不安全问题复现
实例
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:线程2重新获取到时间片,上文判断了数组不会越界,所以进行赋值操作。但是此时的size=3了,再执行element[3]=”b”导致了数组越界。
由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现。
请先
!