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() {
                @Override
                public 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)数组下标越界:
JAVA并发编程篇---ArrayList为什么线程不安全_第1张图片
问题(2) 数组长度问题:
JAVA并发编程篇---ArrayList为什么线程不安全_第2张图片
问题(3):数组元素问题:
JAVA并发编程篇---ArrayList为什么线程不安全_第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:
JAVA并发编程篇---ArrayList为什么线程不安全_第4张图片
JAVA并发编程篇---ArrayList为什么线程不安全_第5张图片
问题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方法的线程安全。保证了数据的一致性,但由于加锁导致访问性能大大降低。
  JAVA并发编程篇---ArrayList为什么线程不安全_第6张图片
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();
        }
}

你可能感兴趣的:(java基础篇,java工具篇,源码解析篇,java,安全,开发语言)