ArrayList源码学习笔记(2)

上一篇文章ArrayList源码学习笔记(1)分析了ArrayList的构造函数和扩容过程,并用demo验证了一下。

现在看到了add方法和remove方法。

add方法有两个add(E e) 和 add(int index, E element),分别是在尾部添加元素、在指定index添加元素。

remove方法也有两个remove(int index) 和 remove(Object o),分别是删除指定索引的元素、删除指定元素(如果存在)。

在add(int index, E element)和remove方法中,都有System.arraycopy操作,需要把一些元素整体后移或者前移。

 

源码比较简单,逻辑很清晰,没什么好说的。但是想到面试的时候总是会问线程安全问题,于是研究了一波。

 

ArrayList是线程安全的吗?

当然不是。

里边没有锁、临界区、volatile等各种为多线程考虑的同步策略。

 

线程不安全的体现是什么?或者为什么线程不安全?

最怕的就是这种问题,因为你需要详实的证据,或者确定的理论依据。

首先你需要知道什么是线程安全,然后才能说一个类是不是线程安全的。

什么是线程安全?

在网上和书中找了很多说法,都不统一。干脆不找准确的了,直接综合各家说法先定下一个概念。

直观的说,线程安全就是多线程对一个变量、对象、临界区进行访问的时候,不会因为线程调度问题而产生不符合预期的结果。

能达到线程安全的情况是:变量只读不可修改、有锁机制保证同时只能有一个线程对变量进行修改。

ArrayList为什么不是线程安全的?

有了上面的理论,现在可以说为啥ArrayList不是线程安全的了。

但是从哪里说起呢?因为ArrayList的线程不安全简直是案例太多了,但我还是找到了核心点来分析。这个核心点是什么呢?那就是会发生改变的共享变量。因为我们的视角其实是在多线程中使用ArrayList,那么ArrayList的对象其实就是一个共享变量,那么它的所有成员变量也就都是共享变量了。在里面挑几个重要的说一下好了。

比较明显的两个变量就是size、elementData数组。这俩肯定是频繁会发生变化的。

下面构造几个场景触发多线程不安全。

1、多线程add
多线程add会有两种线程不安全的表现:(1)对于size的修改冲突,最终size数比预期小(2)有的线程将size增大后,导致另一个线程使用过程中越界。其实这两个问题都是对size变量的修改导致的。

话不多说,上例子。

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 控制主线程等待
        CountDownLatch mainThread = new CountDownLatch(10);
        // 多线程add
        for (int m = 0; m < 10; m++) {
            new Thread(() -> {
                for (int i = 0; i < 10000; i++) {
                    list.add(i);
                }
                mainThread.countDown();
            }).start();
        }

        mainThread.await();

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

一种结果:

Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 4
	at java.util.ArrayList.add(ArrayList.java:463)
	at ArrayListTest.lambda$main$0(ArrayListTest.java:14)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
	at java.util.ArrayList.add(ArrayList.java:463)
	at ArrayListTest.lambda$main$0(ArrayListTest.java:14)
	at java.lang.Thread.run(Thread.java:748)

另一种结果

95491

2、多线程remove

和多线程add道理是一样的,size的并发修改会引发问题。

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 控制主线程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(6);
        // 单线程add
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                list.add(i);
            }
            mainThread.countDown();
        }).start();

        // 多线程remove
        for (int m = 0; m < 5; m++) {
            new Thread(() -> {
                // sleep4秒,使list里积累一些数据
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 10000000; i++) {
                    if (list.size() > 0) {
                        list.remove(list.size() - 1);
                    }
                }
                mainThread.countDown();
            }).start();
        }

        mainThread.await();

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

结果如下:

Exception in thread "Thread-2" java.lang.IndexOutOfBoundsException: Index: 9989869, Size: 9988711
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at ArrayListTest.lambda$main$1(ArrayListTest.java:27)
	at java.lang.Thread.run(Thread.java:748)

3、ArrayList提供的Iterator,不允许在遍历过程中产生修改。

Iterator的remove方法和netx方法会检查在执行过程中是否有修改,如果有修改会报错。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 使主线程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(2);
        // 单线程add
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add(0, i);
            }
            mainThread.countDown();
        }).start();
    
        // 单线程遍历remove
        new Thread(() -> {
            // 等add方法执行5秒,数据积累一些
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Iterator iterator = list.iterator();
            while (iterator.hasNext()) {
                iterator.next();
                // 这个sleep位置在这里,容易触发remove方法里对modcount的检测异常
                // 这个sleep位置在next方法之前,容易触发next方法里对modcount的检测异常
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                iterator.remove();
            }
            mainThread.countDown();
        }).start();

        mainThread.await();

        list.forEach(System.out::println);
    }
}

程序运行结果如下:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.remove(ArrayList.java:873)
	at ArrayListTest.lambda$main$1(ArrayListTest.java:53)
	at java.lang.Thread.run(Thread.java:748)

4、分别有单独的线程进行add和remove

其实,我理解add和remove之间也应该有冲突才对。因为add和remove都会有System.arraycopy操作对数据进行移动,这个应该不是原子的操作,在移动过程中应该是会产生冲突。但具体是产生什么样的冲突我没有想清楚,因为这个方法是native的,没看到它的源码,也不知道它是怎么做的。

设计了下面的代码,但是没有发现有什么错误。(专门用了add(0,i)使每次插入都会调用Syatem.arraycopy;使用remove(0)也会使每次删除都会调用System.arraycopy)

import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;

public class ArrayListTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        ArrayList<Integer> list = new ArrayList<>(0);

        // 使主线程阻塞等待
        CountDownLatch mainThread = new CountDownLatch(5);
        // 单线程add
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                list.add(0, i);
            }
            mainThread.countDown();
        }).start();

        // 单线程remove
        new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                if (list.size() > 0) {
                    list.remove(0);
                }
            }
            mainThread.countDown();
        }).start();

        mainThread.await();
        System.out.println(list.size());
    }
}

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章