JAVA并发编程篇---ArrayList为什么线程不安全

1 测试代码:

package org.lgx.bluegrass.bluegrasscoree.util.tooldemo;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;/*** @Description TODO* @Date 2022/1/7 10:21* @Author lgx* @Version 1.0*/
public class ThreadList {public static void main(String[] args) throws InterruptedException {List<String> lists= new ArrayList<>();CountDownLatch countDownLatch = new CountDownLatch(10);for (int i=0 ;i<10;i++){new Thread(new Runnable() {@Overridepublic void run() {for (int j=0;j<100;j++){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}lists.add(String.valueOf(j));}countDownLatch.countDown();}}).start();}countDownLatch.await();System.out.println(lists.size());for (int i = 0; i < lists.size(); i++) {System.out.println(lists.get(i));}}
}

2 出现问题:
当多线程同时对集合进行元素添加就很大程度出现下面问题:
问题(1)数组下标越界:
在这里插入图片描述
问题(2) 数组长度问题:
在这里插入图片描述
问题(3):数组元素问题:
在这里插入图片描述
3 问题出现原因 add() 方法多线程有问题:

/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return true (as specified by {@link Collection#add})*/public boolean add(E e) {// 这个方法用于进行数组扩容ensureCapacityInternal(size + 1);  // Increments modCount!!// 这个方法用于存放数据elementData[size++] = e;return true;
}

ensureCapacityInternal:
在这里插入图片描述
在这里插入图片描述
问题1数组下标越界:
初始集合长度为10,假如:当前集合长度为9;线程A进行add增加元素发现当前数组长度可以放下新元素,在 elementData[size++] = e;之前线程B 进行add 增加元素因为此时集合的长度依然为9发现当前数组长度可以放下新元素,所以也不进行扩容,也走到
elementData[size++] = e; 这个时候线程A 放入了新元素,这个时候集合的长度为10 最大的下标为9;当线程B 执行 elementData[size++] = e; 在下标为10的地方放入要增加的元素,出现数组下标越界;

问题2的长度问题和问题3的元素数据问题:
elementData[size++] = e;
时间上为以下几个步骤

  • elementData[size] = e;
    size++在cpu执行时又分为三个阶段:
  • 先将 size所在内存的值加载到寄存器;
  • 将寄存器的值自增1;
  • 将寄存器中的值写回内存。

假如此时size为7
当线程A 执行elementData[7] = e;后,在size++前 线程B 进入也执行elementData[7] = e;
这个时候线程B的元素覆盖线程A 的元素,当线程A和B 都执行了size++,实际上只放入了一个元素,此时的size为9,而后续再有线程进行add 添加元素操作,会在最新的下,9放入元素,所以下标为8的位置元素就为null;
如果当线程A和B 都执行了size++ 这个非原子的自增操作后,两个线程各自对当前的长度7进行加1 则此时虽然线程进行了两次add操作但是只添加进去了一个元素,即后面的线程B覆盖了前面线程A的值;

4 那么多线程是怎么使用arrayList?
4.1,用vector类
 vector类的add方法:
Vector lists= new Vector<>(); 替换 List lists= new ArrayList<>();
原理:
  Vector类 是可以实现自动增长的对象数组,其add操作是用synchronized关键字修饰的,从而保证了add方法的线程安全。保证了数据的一致性,但由于加锁导致访问性能大大降低。
  在这里插入图片描述
4.2,使用Collections工具类
List lists= Collections.synchronizedList(new ArrayList<>());替换 List lists= new ArrayList<>();
原理:用synchronized关键字修饰的
在这里插入图片描述
用Collections工具类将线程不安全的ArrayList类转换为线程安全的集合类。小体量数据的ArrayList类可以使用这种方法创建线程安全的类。

4.3,使用CopyOnWriteArrayList类(写时复制,读写分离)
使用:CopyOnWriteArrayList lists = new CopyOnWriteArrayList<>();
替换 List lists= new ArrayList<>();

原理:CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

public boolean add(E e) {//1、先加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;//2、拷贝数组Object[] newElements = Arrays.copyOf(elements, len + 1);//3、将元素加入到新数组中newElements[len] = e;//4、将array引用指向到新数组setArray(newElements);return true;} finally {//5、解锁lock.unlock();}
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部