ArrayList类线程不安全的原因及其解决方法及原理

多线程下ArrayList类线程不安全的解决方法及原理

ArrayList类在多线程环境下是线程不安全的,在多线程读写情况下会抛出并发读写异常(ConcurrentModificationException):

复制代码

 1 import java.util.ArrayList;
 2 import java.util.List;
 3 import java.util.UUID;
 4 
 5 public class NoSafeArrayList {
 6     public static void main(String[] args) {
 7 
 8         List list=new ArrayList();
 9         for (int i=0;i<30;i++) {
10             new Thread(()->{
11                 list.add(UUID.randomUUID().toString().substring(8));  //UUID工具类,取一个八位的随机字符串  ,还有一个常用的取不重复字符串的方法:system.currentTime()  当前时间戳
12                 System.out.println(list);
13             }).start();
14         }
15     }
16 }

复制代码

ArrayList类线程不安全的原因及其解决方法及原理_第1张图片

模拟测试给list加入10000条数据,代码:

 

 
  1. public class UnsafeList {

  2. public static void main(String[] args) {

  3. // 进行 10次测试

  4. for (int i = 0; i < 10; i++) {

  5. test();

  6. }

  7. }

  8.  
  9. public static void test() {

  10. // 用来测试的List

  11. List list = new ArrayList();

  12. // 线程数量(100)

  13. int threadCount = 100;

  14. // 用来让主线程等待threadCount个子线程执行完毕

  15. CountDownLatch countDownLatch = new CountDownLatch(threadCount);

  16. // 启动threadCount个子线程

  17. for (int i = 0; i < threadCount; i++) {

  18. Thread thread = new Thread(new MyThread(list, countDownLatch));

  19. thread.start();

  20. }

  21. try {

  22. // 主线程等待所有子线程执行完成,再向下执行

  23. countDownLatch.await();

  24. } catch (InterruptedException e) {

  25. e.printStackTrace();

  26. }

  27. // List 的size

  28. System.out.println(list.size());

  29. }

  30. }

  31.  
  32. class MyThread implements Runnable {

  33. private List list;

  34. private CountDownLatch countDownLatch;

  35.  
  36. public MyThread(List list, CountDownLatch countDownLatch) {

  37. this.list = list;

  38. this.countDownLatch = countDownLatch;

  39. }

  40.  
  41. public void run() {

  42. // 每个线程向List中添加100个元素

  43. for (int i = 0; i < 1000; i++) {

  44. list.add(new Object());

  45. }

  46. // 完成一个子线程(主线程等待子线程执行完了再执行)

  47. countDownLatch.countDown();

  48. }

  49. }


  50. 打印结果:

     

    100000
    100000
    99847
    100000
    99670
    99442
    99998
    100000
    99271
    99926

    来看看ArrayList.add方法

     

     
    1. // Object[] elementData:ArrayList的数据结构是数组类型,list存放的数据就是存放在elementData里面的

    2. // 第1步

    3. public boolean add(E e) {

    4. ensureCapacityInternal(size + 1); // list的size+1

    5. elementData[size++] = e; // 将数据放到数组最后一个

    6. return true;

    7. }

    8.  
    9.  
    10. // 第2步,元素有变化,那么就调用ensureExplicitCapacity方法

    11. private void ensureCapacityInternal(int minCapacity) {

    12. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

    13. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

    14. }

    15.  
    16. // 进入ensureExplicitCapacity方法

    17. ensureExplicitCapacity(minCapacity);

    18. }

    19.  
    20.  
    21. // 第3步,元素有变化,那么就调用grow方法

    22. private void ensureExplicitCapacity(int minCapacity) {

    23. modCount++;

    24. // elementData:list的数组元素

    25. // minCapacity: add操作后的容量

    26. if (minCapacity - elementData.length > 0)

    27. grow(minCapacity);

    28. }

    29.  
    30.  
    31. // 第4步

    32. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 为什么要-8,是因为有些虚拟机有一些hear的key

    33. private void grow(int minCapacity) {

    34.  
    35. // 原始list的容量(容量不是list.size)

    36. int oldCapacity = elementData.length;

    37.  
    38. //现在list的容量,此时是做讲原始容量扩大0.5倍,oldCapacity >> 1:2进制右位移,就是除以2的意思

    39. int newCapacity = oldCapacity + (oldCapacity >> 1);

    40. if (newCapacity - minCapacity < 0)

    41. newCapacity = minCapacity;

    42. // 一般不会进入hugeCapacity这个方法,

    43. if (newCapacity - MAX_ARRAY_SIZE > 0)

    44. newCapacity = hugeCapacity(minCapacity);

    45.  
    46. // 复制elementData返回一个新的数组对象,这个时候list.add完成

    47. elementData = Arrays.copyOf(elementData, newCapacity);

    48. }

     

    分析为什么会add丢失呢?

    List对象,做add时,第1步到第3步,都不会改变elementData对象,只有在第4步Arrays.copyOf的时候,返回一个新的数组对象
    因此:当有线程t1、t2同时进入grow方法,两个线程都会执行Arrays.copyOf方法,返回2个不同的elementData对象,
    假如,t1先返回,t2后返回,那么List.elementData == t1.elementData,
    然后t2也返回后,这时List.elementData == t2.elementData
    这时,t2.elementData就把t1.elementData数据给覆盖了。导致t1.elementData被丢失

    解决方法:

    1,用vector类

      Vector类 是可以实现自动增长的对象数组,其add操作是用synchronized关键字修饰的,从而保证了add方法的线程安全。保证了数据的一致性,但由于加锁导致访问性能大大降低。

      vector类的add方法:

    ArrayList类线程不安全的原因及其解决方法及原理_第2张图片

     

    2,使用Collections工具类

    用Collections工具类将线程不安全的ArrayList类转换为线程安全的集合类。小体量数据的ArrayList类可以使用这种方法创建线程安全的类。

    1 List list=Collections.synchronizedList(new ArrayList);

    3,使用CopyOnWriteArrayList类(写时复制,读写分离)

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    复制代码

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

    复制代码

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

    线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
    1、如果写操作未完成,那么直接读取原数组的数据;
    2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
    3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

    你可能感兴趣的:(多线程)