ArrayList线程安全问题以及解决方案

1. 多线程下的ArrayList

public class ConcurrentArrayList {
    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>();

        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        };
        
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        
        Thread.sleep(500);
        System.out.println(list.size());
    }
}

上述运行可能会出现
代码中循环创建了两个线程,这两个线程都执行10000次数组的添加操作,理论上最后输出的结果应该为20000,但经过多次尝试,最后只出现了两种结果:

1.数组索引越界异常
2.list.size()不为20000

看一下add()方法的代码:

	
    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. 扩充数组长度;

增加元素过程中较为容易出现问题的部分在于elementData[size++] = e;.赋值的过程可以分为两个步骤elementData[size] = e;size++;

我们分别使用两个线程来模拟插入过程.例如有两个线程,分别加入数字1与2.

在这里插入图片描述

运行的过程如下所示:

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

产生了null值 以及数据丢失的问题

数组越界情况. 我们将上方的线程运行图更新下进行演示:

在这里插入图片描述

前提条件: 当前size=2 数组长度为2.

线程1 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
线程2 判断数组是否越界.因为size=2 长度为2,没有越界.将进行赋值操作.但是因为时间片问题导致了中断.
线程1 重新获取到主动权.上文判断了长度刚刚好够用.进行赋值操作element[size]=1,并且size++
线程2 因为上文判断了数组没有越界.所以进行赋值操作.但是此时的size=3了.再执行element[3]=2. 导致了数组越界了.
由此处可以看出因为数组的当前指向size并未进行加锁的操作,导致了数组越界的情况出现.

核心问题就在于 elementData[size++] = e;不是原子操作

2.线程安全的list解决方案:

有三种方式:

  1. vector 在add方法上加上synchronized锁

  2. Collections的静态方法synchronizedList(List< T> list)

  3. copyonwritearraylist写时复制的思想

1.使用Vector容器

Vector类实现了可扩展的对象数组,并且它是线程安全的。Vector的解决方案很简单,它采用了同步关键词synchronized修饰方法。

public void add(int index, E element) {
    insertElementAt(element, index);
}
...
// 使用了synchronized关键词修饰
public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }

可以看出,Vector在通用方法的实现上ArrayList并没有什么区别(这里不比较扩容方式等细节)

2. Collections.synchronizedList(List< T> list)
/**
     * Returns a synchronized (thread-safe) list backed by the specified
     * list.  In order to guarantee serial access, it is critical that
     * <strong>all</strong> access to the backing list is accomplished
     * through the returned list.<p>
     *
     * It is imperative that the user manually synchronize on the returned
     * list when iterating over it:
     * <pre>
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>
     * Failure to follow this advice may result in non-deterministic behavior.
     *
     * <p>The returned list will be serializable if the specified list is
     * serializable.
     *
     * @param  <T> the class of the objects in the list
     * @param  list the list to be "wrapped" in a synchronized list.
     * @return a synchronized view of the specified list.
     */
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

因为ArrayList实现了RandomAccess接口,因此该方法返回一个SynchronizedRandomAccessList实例。
该类的add实现:

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}

其中,mutex是final修饰的一个对象:

final Object mutex;

我们可以看到,这种线程安全容器是通过同步代码块来实现的,基础的add方法任然是由ArrayList实现。

我们再来看看它的读方法:

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

和写方法没什么区别,同样是使用了同步代码块。线程同步的实现原理非常简单!

通过上面的分析可以看出,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此某些情况下它并不是一个好的选择。针对这个问题,我们引出第三种线程安全容器的实现。

3. CopyOnWriteArrayList

CopyOnwriteArrayList 实现了 读 读操作和 读写操作不互斥

实现

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();
    }
    
}

可以看到是通过JUC包下的lock来实现线程间的同步的,

怎么实现读写不互斥的呢?

在面临写操作的时候,CopyOnwriteArrayList会先复制原来的数组并且在新数组上进行修改,最后再将原数组覆盖。如果写操作过程中发生了线程切换。并且切换到读线程,因为此时数组并未发生覆盖,读操作读取的还是原数组。

另外,数组定义private transient volatile Object[] array,其中采用volatile修饰,保证内存可见性,读取线程可以马上知道这个修改。

private transient volatile Object[] array;

也就是说当读写并发时 读操作是在旧数组中 读到的旧值

三种方式的性能比较

1. 首先我们来看看三种方式在写操作的情况:
public class ConcurrentList {
    public static void main(String[] args) {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    private static void testVector() {

        Vector vector = new Vector();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            vector.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));

    }

    public static void testSynchronizedList(){
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList(){
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        long time1 = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            list.add(i);
        }
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }

}

在代码中我让Vector和SynchronizedList两种实现方式进行写操作10000000次,而CopyOnWriteArrayList仅仅只有100000次,与前两种方式少了100倍!但是耗时缺差不多

在这里插入图片描述

第三种方式使用的时间远大于前两种,写操作越多,时间差就越明显。

看似出乎意料,实则意料之中,copyOnWriteArrayList每进行一次写操作都会复制一次数组,这是非常耗时的操作,因此在面临巨大的写操作量时才会差异这么大。

而对于 vector和SynchronizedList的性能上的差别 主要是由于synchronized加载实例方法和加载代码块上的差别引起的,同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点,一般同步的范围越大,性能就越差,一般需要加锁进行同步的时候,肯定是范围越小越好,这样性能更好。

如果开两个线程的情况下

public class ConcurrentList {
    public static void main(String[] args) throws InterruptedException {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    private static void testVector() throws InterruptedException {
        Vector vector = new Vector();
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 10000000; i++) {
                    vector.add(i);
                }
                semaphore.release();

            }
        };

        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));

    }

    public static void testSynchronizedList() throws InterruptedException {
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.add(i);
                }
                semaphore.release();
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList() throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    list.add(i);
                }
                semaphore.release();
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }

}

vector和synchorinzedList性能差不多

在这里插入图片描述

2. 我们再来看看三种方式在读操作的情况:
public class ConcurrentList2 {
    public static void main(String[] args) throws InterruptedException {
        testVector();
        testSynchronizedList();
        testCopyOnWriteArrayList();
    }

    public static void testVector() throws InterruptedException {
        Vector<Integer> vector = new Vector<>();
        vector.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    vector.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("vector: "+(time2-time1));
    }

    public static void testSynchronizedList() throws InterruptedException {
        List<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());
        list.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("synchronizedList: "+(time2-time1));
    }

    public static void testCopyOnWriteArrayList() throws InterruptedException {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        list.add(0);
        Semaphore semaphore = new Semaphore(0);
        long time1 = System.currentTimeMillis();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000000; i++) {
                    list.get(0);
                }
                semaphore.release(1);
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runnable).start();
        }
        semaphore.acquire(2);
        long time2 = System.currentTimeMillis();
        System.out.println("copyOnWriteArrayList: "+(time2-time1));
    }
}

在这里插入图片描述

测试方式不严谨哈 读写并发就不测了

可以看出 CopyOnWriteArrayList由于读不加锁 在读的性能上 非常好

总结

  1. 获取线程安全的List我们可以通过Vector、Collections.synchronizedList()方法和CopyOnWriteArrayList三种方式
  2. 得益于读写不冲突,以及读不加锁,在读多写少的情况下,推荐使用CopyOnWriteArrayList方式
  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的ArrayList是线程不安全的,如果多个线程同时对ArrayList进行修改操作可能导致数据不一致或者出现异常。为了解决这个问题,可以使用以下几种方法实现线程安全ArrayList: 1. 使用Collections的synchronizedList方法 ```java List list = Collections.synchronizedList(new ArrayList()); ``` 这种方法是将ArrayList转换成线程安全的List,它的原理是在每个方法中加上了synchronized关键字,从而保证了线程安全。但是这种方法有一个缺点,就是当多个线程同时执行操作时,只有一个线程能够访问List,其他线程需要等待,影响了程序效率。 2. 使用CopyOnWriteArrayList ```java List list = new CopyOnWriteArrayList(); ``` CopyOnWriteArrayListJava并发包中提供的线程安全的List,它的原理是在对List进行修改操作时,会先复制一份List,然后在副本上进行操作,最后再将副本赋值给原来的引用。因为每次修改操作都是对副本进行的,所以不会影响到原来的List,从而保证了线程安全。但是这种方法有一个缺点,就是在进行修改操作时会创建一个新的副本,占用了一定的内存空间。 3. 使用Lock和Condition ```java import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class MyList { private ArrayList<Integer> list; private final Lock lock; private final Condition notEmpty; public MyList() { list = new ArrayList<>(); lock = new ReentrantLock(); notEmpty = lock.newCondition(); } public void add(int value) { lock.lock(); try { list.add(value); notEmpty.signalAll(); } finally { lock.unlock(); } } public int remove() throws InterruptedException { lock.lock(); try { while (list.size() == 0) { notEmpty.await(); } return list.remove(0); } finally { lock.unlock(); } } } ``` 这种方法使用了Lock和Condition来实现线程安全,它的原理是在对List进行修改操作时,使用Lock来保证只有一个线程能够修改List,其他线程需要等待,直到修改完成后再唤醒它们。同时使用Condition来保证当List为空时,消费线程会等待生产线程向List中添加元素。 总之,以上三种方法都可以实现线程安全ArrayList,不同的方法有不同的优缺点,在实际使用时需要根据具体情况选择适合的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值