ArrayList为什么线程不安全以及三种解决办法【详细】

不安全原因

  • 我们可以看一下ArrayList源码,找到add方法,
public boolean add(E e) {
    
    
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

从上面的代码可以看出,add()方法没有使用同步互斥,所以在多线程并发中,会出现线程异常,测试代码:

import java.util.ArrayList;
import java.util.UUID;

public class SetUnsefertyTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建ArrayList 集合
        ArrayList<String> list = new ArrayList<>();

        // 创建10个线程,往 list 中添加元素
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(()->{
    
    
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                // 从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

出现异常:
请添加图片描述

解决办法

Vector

可以看一下Vector的add源码,加上了synchronized同步关键字
但是 Vector 用的不多,因为每次对添加的元素上锁,而且使用的是重量级锁synchronized是十分占用资源的,效率是十分低下的。其用法和 ArrayList 一样。

public synchronized boolean add(E e) {
    
    
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

测试代码:

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Vector;

public class SetUnsefertyTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建ArrayList 集合
        List<String> list = new Vector<String>();

        // 创建10个线程,往 list 中添加元素
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(()->{
    
    
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                // 从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

Collections

进入 Collections 的底层,找到 synchronizedList(List list) 方法,源代码如下,synchronizedList(List list) 方法返回指定列表支持的同步(线程安全的)列表

public static <T> List<T> synchronizedList(List<T> list) {
    
    
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static <T> List<T> synchronizedList(List<T> list, Object mutex) {
    
    
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list, mutex) :
            new SynchronizedList<>(list, mutex));
}

对 Collections 的使用如下

List<String> list = Collections.synchronizedList(new ArrayList<>());

测试代码:

import java.util.*;

public class SetUnsefertyTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建ArrayList 集合
//        List<String> list = new Vector<String>();

        List<String> list = Collections.synchronizedList(new ArrayList<>()) ;
        // 创建10个线程,往 list 中添加元素
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(()->{
    
    
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                // 从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

CopyOnWriteArrayList

这是写时复制思想, 首先看add()方法中有可重入锁,这个目的是防止多个线程争抢写的权力,然后下面红框中的内容是将原件复制出来一份,然后在复印件上写,之后通过setArray()方法让原件地址指向复印件,这样可以让所有人读原件,而我只修改复印件,所以读和写不会出现冲突,因此通过加锁和写时复制思想可以很好保证了多线程情况下所有线程都可以读,但是只有一个线程在写,因此不会出现并发修改异常,如源码:

public boolean add(E e) {
    
    
    // 声明一个重进入锁
    final ReentrantLock lock = this.lock;
    // 上锁
    lock.lock();
    try {
    
    
        // 获取原来的列表
        Object[] elements = getArray();
        // 获取原来列表的长度
        int len = elements.length;
        // 复制一个与原来的列表一样的列表
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 将新加入的元素放到列表末尾
        newElements[len] = e;
        // 旧新合并
        setArray(newElements);
        return true;
    } finally {
    
    
        // 解锁
        lock.unlock();
    }
}

测试代码:

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class SetUnsefertyTest {
    
    
    public static void main(String[] args) {
    
    
        // 创建ArrayList 集合
//        List<String> list = new Vector<String>();

//        List<String> list = Collections.synchronizedList(new ArrayList<>()) ;
        List<String> list =new CopyOnWriteArrayList<>() ;
        // 创建10个线程,往 list 中添加元素
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(()->{
    
    
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0,8));
                // 从集合中取出内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

三种解决方式总结

对比三者来看,Vector和Collections虽然也可以实现同步,但由于这两种方法在底层都使用了synchronized重量级锁,使其效率很低,所以对 ArrayList 的同步主要采用 CopyOnWriteArrayList

猜你喜欢

转载自blog.csdn.net/weixin_54046648/article/details/128174145