ArrayList为什么会出现并发问题以及相应的解决办法

问题一:ArrayList为什么会出现并发问题?

ArrayList是线程不安全的,在多线程并发访问的时候可能会出现问题,如果想使用线程安全的集合类,java自带有vector,也就是说vector是线程安全的。但是arayList的底层是数组实现的,而且可以自动扩容,获得元素或者在数组尾段插入元素的效率高,所以说ArrayList有其独特的优势。

1.扩容实现

private transient Object[] elementData;
 
public void ensureCapacity(int minCapacity) {
	modCount++;
	int oldCapacity = elementData.length;
	if (minCapacity > oldCapacity) {
	    Object oldData[] = elementData;
	    int newCapacity = (oldCapacity * 3)/2 + 1;
    	    if (newCapacity < minCapacity)
		newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
	}
    }

 

elementData数组的初始容量是10,如果需要扩容时,使用 elementData = Arrays.copyOf(elementData, newCapacity)进行复制, elementData = Arrays.copyOf(elementData, newCapacity)底层的核心代码为:

System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
该方法中,original指的是:elementData,旧数组,0指的是复制从elementDate的索引为0开始

copy新生成的数组,0,新数组的索引为0开始,最后一个参数Math.min(elementData.length,newCapacity)

在上面的源码中,它的意思就是把老数组中的数据复制到一个新生成的容量为newLength的数组中。

扩容的时候,如果在并发操作ArrayList的时候,可能会有数组索引越界的异常产生。

分析上源码可以看出,只有当minCapacipity>oldCapacipity时才发生扩容问题,假设minCapacipity=oldCapacipity=10,不发生扩容问题,元素是可以被插入的。大家可以看下面的add源码,也就是size=9时可以将元素添加到数组中,多线程示意图如下:

ArrayList为什么会出现并发问题以及相应的解决办法_第1张图片

线程A和线程B获取的size都是9,线程A先插入元素e,这个时候elementData数组的大小为10,是正常情况下下次应该是要扩容的,但是线程B获取的size=9而不是10,在线程B中没有进行扩容,而是报出数组index越界异常。

2.add操作

public boolean add(E e) {
	ensureCapacity(size + 1);  // Increments modCount!!
	elementData[size++] = e;
	return true;
    }

add操作中先进行扩容操作ensureCapacity(size+1),之后才添加数据到elementData这个数组的末端,但是这样的操作不是线程安全的,多线程操作的时候可能会出现数据覆盖的问题。

线程A执行了ArrayList的add方法,由于线程B获取到的size大小和线程A是一样的,此时的size大小应该是比原来的size要大1,但是B线程不知,所以B线程进行赋值的时候把A线程的值给覆盖,导致添加到数组中元素的个数其实是比逻辑上要少的。

ArrayList为什么会出现并发问题以及相应的解决办法_第2张图片

package TestArrayList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {

	private static List list = new ArrayList();

	private static ExecutorService executorService = Executors.newFixedThreadPool(1000); //定长线程池1000

	private static class IncreaseTask extends Thread{
	    @Override
	    public void run() {
	        System.out.println("ThreadId:" + Thread.currentThread().getId() + " start!");
	        for(int i =0; i < 100; i++){
	            list.add(i);
	        }
	        System.out.println("ThreadId:" + Thread.currentThread().getId() + " finished!");
	    }
	}

	public static void main(String[] args){
	    for(int i=0; i < 1000; i++){ //开启1000个线程
	        executorService.submit(new IncreaseTask());
	    }
	    executorService.shutdown();
	    while (!executorService.isTerminated()){
	        try {
	            Thread.sleep(1000*10);
	        }catch (InterruptedException e){
	            e.printStackTrace();
	        }
	    }
	    System.out.println("All task finished!");
	    System.out.println("list size is :" + list.size());
	}
}

结果:list某个随机值是99900,本应该是100000,所以add操作是线程不安全的。

问题二:如何避免ArrayList的并发问题?

(1)使用Collections.synchronizedList()方法对ArrayList对象进行包装

ArrayList arraylist = Collections.synchronizedList(new ArrayList());

将问题一中的Arraylist改成Collections.synchronizedList(new ArrayList)生成,问题的结果是100000.

源码:

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

ArrayList为什么会出现并发问题以及相应的解决办法_第3张图片

上述类的继承结构如上所示,SynchronizedList是SynchronizedRandomAccessList的父类,我们现在看下SynchronizedList的源码,了解下为什么SynchronizedList是线程安全的。

	SynchronizedList(List list) {
	    super(list);
	    this.list = list;
	}
	SynchronizedList(List list, Object mutex) {
            super(list, mutex);
	    this.list = list;
        }

	public boolean equals(Object o) {
	    synchronized(mutex) {return list.equals(o);}
        }
	public int hashCode() {
	    synchronized(mutex) {return list.hashCode();}
        }

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

	public int indexOf(Object o) {
	    synchronized(mutex) {return list.indexOf(o);}
        }
	public int lastIndexOf(Object o) {
	    synchronized(mutex) {return list.lastIndexOf(o);}
        }

	public boolean addAll(int index, Collection c) {
	    synchronized(mutex) {return list.addAll(index, c);}
        }

	public ListIterator listIterator() {
	    return list.listIterator(); // Must be manually synched by user
        }

	public ListIterator listIterator(int index) {
	    return list.listIterator(index); // Must be manually synched by user
        }

	public List subList(int fromIndex, int toIndex) {
	    synchronized(mutex) {
                return new SynchronizedList(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

        /**
         * SynchronizedRandomAccessList instances are serialized as
         * SynchronizedList instances to allow them to be deserialized
         * in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
         * This method inverts the transformation.  As a beneficial
         * side-effect, it also grafts the RandomAccess marker onto
         * SynchronizedList instances that were serialized in pre-1.4 JREs.
         *
         * Note: Unfortunately, SynchronizedRandomAccessList instances
         * serialized in 1.4.1 and deserialized in 1.4 will become
         * SynchronizedList instances, as this method was missing in 1.4.
         */
        private Object readResolve() {
            return (list instanceof RandomAccess
		    ? new SynchronizedRandomAccessList(list)
		    : this);
        }
    }

关于mutex定义:


	final Collection c;  // Backing Collection
	final Object mutex;     // Object on which to synchronize

	SynchronizedCollection(Collection c) {
            if (c==null)
                throw new NullPointerException();
	    this.c = c;
            mutex = this;
        }
	SynchronizedCollection(Collection c, Object mutex) {
	    this.c = c;
            this.mutex = mutex;
        }

易知,mutex指向的就是当前对象自己,所以SynchronizedList是线程安全的根本原因是使用Synchronized对SynchronizedList的add,delete等操作进行加锁,但是这种锁的力度很大,所以这种方式效率低下。

(2)使用并发容器CopyOnWriteArrayList

CopyOnWriteArrayList list = new CopyOnWriteArrayList();

源码:

    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    transient final ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private volatile transient Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }
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();
	}
    }

从add方法知道:CopyOnWriteArrayList底层数组的扩容方式是一个一个地增加,而且每次把原来的元素通过Arrays.copy()方法copy到新数组中,然后在尾部加上新元素e.它的底层并发安全的保证是通过ReentrantLock进行保证的,CopyOnWriteArrayList和SynchronizedList的底层实现方式是不一样的,前者是通过Lock机制进行加锁,而后者是通过Synchronized进行加锁,至于两者的区别,下次详细描述。

对比实验

将问题一中的测试代码,修改如下,对比SynchronizedList和CopyOnWriteArrayList的执行时间:

package TestArrayList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
	
     //private static List list = Collections.synchronizedList(new ArrayList());
	private static CopyOnWriteArrayList list = new CopyOnWriteArrayList();

	private static ExecutorService executorService = Executors.newFixedThreadPool(1000); //定长线程池1000

	private static class IncreaseTask extends Thread{
	    @Override
	    public void run() {
	        System.out.println("ThreadId:" + Thread.currentThread().getId() + " start!");
	        for(int i =0; i < 100; i++){
	            list.add(i);
	        }
	        System.out.println("ThreadId:" + Thread.currentThread().getId() + " finished!");
	    }
	}

	public static void main(String[] args){
		long start = System.currentTimeMillis();
	    for(int i=0; i < 1000; i++){ //开启1000个线程
	        executorService.submit(new IncreaseTask());
	    }
	    executorService.shutdown();
	    while (!executorService.isTerminated()){
	        try {
	            Thread.sleep(1000*10);
	        }catch (InterruptedException e){
	            e.printStackTrace();
	        }
	    }
	    System.out.println("All task finished!");
	    System.out.println("list size is :" + list.size());
	    long end = System.currentTimeMillis();
	    System.out.println("operation time:"+(double)(end-start)/1000+"s");
	}
}

实验结果:

ArrayList为什么会出现并发问题以及相应的解决办法_第4张图片ArrayList为什么会出现并发问题以及相应的解决办法_第5张图片

左边是SynchronizedList,右边是CopyOnWriteArrayList,分析表明两者差别不大。

参考:

【1】http://www.cnblogs.com/areyouready/p/6803304.html

【2】https://www.cnblogs.com/vitasyuan/p/8955557.html

【3】https://blog.csdn.net/qing337197645/article/details/51219155

【4】https://my.oschina.net/jielucky/blog/167198

 

 

你可能感兴趣的:(java基础)