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)数组下标越界:
问题(2) 数组长度问题:
问题(3):数组元素问题:
3 问题出现原因 add() 方法多线程有问题:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (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();
}
}