大家都知道ArrayList是线程不安全的,那我们如何写一个demo来证明ArrayList是线程不安全的呢,并且我们有哪些方式可以解决ArrayList线程不安全呢?
首先,我们创建30个线程去往ArrayList中添加元素:
public class ArrayListDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
//此处只是案例demo,真实使用线程池不建议用这种方式创建
ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 1; i <= 100; i++) {
int finalI = i;
threadPool.execute(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(finalI);
});
}
threadPool.shutdown();
} }
运行程序:
程序报了数组下标越界异常异常ArrayIndexOutOfBoundsException,我们也可以通过看下ArrayList底层中add()方法,是没有加锁的操作,当多个线程共享一份资源时,可能发生线程问题;
ArrayList的add()方法是没有加锁
那我们如何解决ArrayList线程不安全的问题呢
一、使用Vector
Vector是线程安全的,我们可以看下Vector底层的方法是同步的(Synchronized修饰),从而可以解决ArrayList线程不安全的问题;
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
二、使用Collections.synchronizedList()来解决ArrayList线程不安全的问题
将上述案例中创建list方法改成如下即可
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedList()方法中会这个根据传入的List是否实现RandomAccess这个接口来返回的SynchronizedRandomAccessList还是SynchronizedList.
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
我们可以看下SynchronizedList中的方法也是同步的,使用了Synchronized修饰;
上面两种方法确实是可以解决ArrayList线程不安全的问题,但是两者都是将所有的方法都加锁,那会导致效率低下,只能一个线程操作完,下一个线程获取到锁才能操作;
我们来认识一下:CopyOnWriteArrayList
CopyOnWriteArrayList是如何保证线程安全的呢?
CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理是读写分离,其对写操作使用ReentrantLock来上锁,对读操作则不加锁;CopyOnWriteArrayList在写操作的时候,会将list中的数组拷贝一份副本,然后对其副本进行操作(如果此时其他线程需要读的事,那么其他线程读取的是原先的没有修改的数组,如果其他写操作的线程要进行写操作,需要等待正在写的线程操作完成,释放ReentrantLock后,去获取锁才能进行写操作),写操作完成后,会讲list中数组的地址引用指向修改后的新数组地址。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();
}
}
我们可以看到,如果使用场景的写操作十分频繁的话,建议还是不要实现CopyOnWriteArrayList,因为其添加的时候会造成数组的不断扩容和复制,十分消耗性能,会消耗内存,如果原数组的数据比较多的情况下,可能会导致young gc或者full gc;并且其不能使用在实时读的场景,在写操作过程中是要花费时间的,读取的时候可能还是旧数据;
CopyOnWriteArrayList 合适读多写少的场景, 如果我们在使用的时候没法保证CopyOnWriteArrayList 到底要放多少数据的话,我们还是要谨慎使用,如果数据稍微有点多,每次写操作都重新拷贝数组,其代价实在太高昂了。