问题一: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的时候,可能会有数组索引越界的异常产生。

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

线程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线程的值给覆盖,导致添加到数组中元素的个数其实是比逻辑上要少的。

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<Integer> list = new ArrayList<Integer>();private static ExecutorService executorService = Executors.newFixedThreadPool(1000); //定长线程池1000private static class IncreaseTask extends Thread{@Overridepublic 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<Integer> arraylist = Collections.synchronizedList(new ArrayList());

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

源码:

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

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

 SynchronizedList(List<E> list) {super(list);this.list = list;}SynchronizedList(List<E> 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<? extends E> c) {synchronized(mutex) {return list.addAll(index, c);}}public ListIterator<E> listIterator() {return list.listIterator(); // Must be manually synched by user}public ListIterator<E> listIterator(int index) {return list.listIterator(index); // Must be manually synched by user}public List<E> subList(int fromIndex, int toIndex) {synchronized(mutex) {return new SynchronizedList<E>(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<E>(list): this);}}

关于mutex定义:

final Collection<E> c;  // Backing Collectionfinal Object mutex;     // Object on which to synchronizeSynchronizedCollection(Collection<E> c) {if (c==null)throw new NullPointerException();this.c = c;mutex = this;}SynchronizedCollection(Collection<E> c, Object mutex) {this.c = c;this.mutex = mutex;}

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

(2)使用并发容器CopyOnWriteArrayList

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();

源码:

    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<Integer> list = Collections.synchronizedList(new ArrayList<Integer>());private static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();private static ExecutorService executorService = Executors.newFixedThreadPool(1000); //定长线程池1000private static class IncreaseTask extends Thread{@Overridepublic 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");}
}

实验结果:

左边是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

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

  1. 高并发是什么和如何解决

    之前我将高并发的解决方法误认为是线程或者是队列可以解决,因为高并发的时候是有很多用户在访问,导致出现系统数据不正确.丢失数据现象,所以想到 的是用队列解决,其实队列解决的方式也可以处理,比如我们在竞拍 ...

  2. ArrayList问题之高并发多线程环境下会出现内部成员会出现null

    问题描述 最近在做飞机大战游戏,发现一个问题,就是游戏运行了一定时间后会停止,同时eclipse会报出空指针异常. 为此,我针对性地在 遍历list列表并取出列表元素 的for循环中输出每个对象,发现 ...

  3. Java 集合系列(3): fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)...

    戳上面的蓝字关注我们哦! 精彩内容 精选java等全套视频教程 精选java电子图书 大数据视频教程精选 java项目练习精选 概要 前面,我们已经学习了ArrayList.接下来,我们以ArrayL ...

  4. java操作redis并发_使用Redis incr解决并发问题的操作

    项目背景: 1.新增问题件工单,工单中有工单编码字段,工单编码字段的规则为 "WT"+yyyyMMdd+0000001. 2.每天的工单生成量是30W,所以会存在并发问题 解决思路 ...

  5. java雪崩_【并发编程】java 如何解决redis缓存穿透、缓存雪崩(高性能示例代码)...

    [并发编程]java 如何解决redis缓存穿透.缓存雪崩(高性能示例代码) 发布时间:2018-11-22 16:48, 浏览次数:872 , 标签: java redis <>缓存穿透 ...

  6. java 时间戳_Java并发编程之CAS三CAS的缺点 及解决办法

    Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...

  7. 高并发架构系列:如何解决Redis雪崩、穿透、并发等5大难题

    别人用手机刷新闻.刷段子,你用手机刷知识.你会的越多,成功率就越高. 本篇分享大型网站高并发架构设计是如何解决Redis雪崩.穿透.并发等5大难题的,以下,enjoy~ 缓存雪崩 数据未加载到缓存中, ...

  8. Java 集合系列04之 fail-fast总结(通过ArrayList来说明fail-fast的原理、解决办法)

    http://www.cnblogs.com/skywang12345/p/3308762.html 概要 前面,我们已经学习了ArrayList.接下来,我们以ArrayList为例,对Iterat ...

  9. loadrunner录制IE11脚本并发有兼容性问题的解决方法

    loadrunner11不支持IE11,而loadrunner12永久免费试用50个虚拟用户并发但是不能x(破)x(解). 说说我的解决办法: 1.50虚拟用户以内用LR12别犹豫,LR12录制脚本兼 ...

最新文章

  1. python小游戏代码大全-python贪吃蛇游戏代码
  2. jsp servlet table 集合list 数据 绑定
  3. android中组件获取焦点
  4. 小米12 Ultra延期发布:或与小米MIX Fold 2折叠屏旗舰同台亮相
  5. arm 模式 Linux,ARM Linux:usr模式转为svc模式的实现原理
  6. Win10下python3和python2多版本同时安装并解决pip共存问题
  7. if/else if多分支语句(JS)
  8. 例解回归分析(原书第5版)
  9. 工业和信息化部教育考试中心职业技术证书有必要考吗?
  10. csrss.exe病毒的清除方法
  11. win10打开无线网卡服务器,win10打不开无线网卡
  12. python pandas 安装time out
  13. S3C2440移植linux3.4.2内核之内核框架介绍及简单修改
  14. mysql 查询半径范围内经纬度坐标
  15. ubuntu14.04编译安装strongswan
  16. MySQL 内连接、外连接、全连接
  17. python语言玫瑰花_Python 玫瑰花绘制
  18. Qt3升级 -Qt论坛问答翻译
  19. 中关村工业互联网产业联盟成立大会成功召开
  20. 华为nova8se和vivoS7e的区别哪个好

热门文章

  1. mysql 页分裂_mysql聚簇索引的页分裂原理实例分析
  2. [1153]mysql中between的边界范围
  3. 数据架构——企业数据
  4. 银笺生花,伴你浪迹天涯
  5. PSW标志寄存器中的ZF、OF、SF、CF字段
  6. 老师计算机组合照说说,老师发表的说说
  7. java火焰_使用linux perf工具生成java程序火焰图
  8. 摄影怎么搞副业好?摄影副业是如何赚钱的呢?
  9. java项目内存分析jmap命令+MAT工具
  10. 《阿甘正传》--简单中的美好