所有分类
  • 所有分类
  • 未分类

Java-ArrayList保证线程安全的方法

简介

本文介绍Java中的ArrayList、LinkedList如何进行线程安全的操作、为什么ArrayList不是线程安全的。

这几个问题也是Java后端面试中经常问到的问题。

线程安全的操作方法

ArrayList

方法示例原理
VectorList 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中的 CopyOnWriteArrayListCopyOnWriteArrayList<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中的ConcurrentLinkedQueueConcurrentLinkedQueue<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
...

总共有四种情况:

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

如果四条语句按照: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”:

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

前提条件: 当前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:线程2重新获取到时间片,上文判断了数组不会越界,所以进行赋值操作。但是此时的size=3了,再执行element[3]=”b”导致了数组越界。

由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现。

16

评论5

请先

  1. 但是因为时间片用完而中断 是什么意思?
    修身立德 2024-10-08 0
    • 这个与linux操作系统有关,CPU是通过时间片去执行任务的,可以网上搜一下
      自学精灵 2024-10-08 0
  2. 看到解决方法了没有问题了
    lxj_wzl 2024-01-07 0
    • 是的,解决方法在文章最前边:线程安全的操作方法
      自学精灵 2024-01-07 0
  3. 这一部分不全吧,没有解决方法
    lxj_wzl 2024-01-07 1
显示验证码
没有账号?注册  忘记密码?

社交账号快速登录