Анализ исходного кода CopyOnWriteArrayList

Java

Параллельные пакеты Java предоставляют множество потокобезопасных коллекций.Благодаря их существованию мы можем писать код так же, как однопоточный при многопоточной разработке, что значительно упрощает сложность многопоточной разработки, но если вы не знаете принцип., может вызвать неожиданные проблемы, поэтому необходимо знать принцип.

Сегодня мы посмотрим на Java и Thread-Safe List, предоставляемый в договоре, то есть peporonwritearraylist.

Когда я был новичком в CopyOnWriteArrayList, мне всегда казалось, что название этой коллекции немного странное: копировать при записи? Только позже я узнал, что он был скопирован во время написания, поэтому именование все еще довольно строгое. Конечно, лучше было бы перевести на копирование при записи.

Когда мы изучаем исходный код, мы можем изучать его с помощью вопросов, которые могут быть более эффективными, разбирать проблемы одну за другой и иметь чувство достижения, поэтому здесь я сначала задам несколько вопросов:

  1. Как CopyOnWriteArrayList гарантирует потокобезопасность.
  2. Длина CopyOnWriteArrayList не ограничена.
  3. Почему CopyOnWriteArrayList является коллекцией с копированием при записи.

Давайте сначала посмотрим на UML-диаграмму CopyOnWriteArrayList:

Основной метод Исходный код Анализ

add

Мы можем добавить элемент через метод Добавить

    public boolean add(E e) {
        //1.获得独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//2.获得Object[]
            int len = elements.length;//3.获得elements的长度
            Object[] newElements = Arrays.copyOf(elements, len + 1);//4.复制到新的数组
            newElements[len] = e;//5.将add的元素添加到新元素
            setArray(newElements);//6.替换之前的数据
            return true;
        } finally {
            lock.unlock();//7.释放独占锁
        }
    }

    final Object[] getArray() {
        return array;
    }

Когда вызывается метод add, код переходит к (1) для получения монопольной блокировки.Из-за характеристик монопольной блокировки, если несколько потоков запускаются в (1) одновременно, только один поток может успешно получить монопольную блокировку. эксклюзивную блокировку и выполнить следующий код, остальные потоки могут только ждать снаружи, пока эксклюзивная блокировка не будет снята.

После того, как поток получает эксклюзивную блокировку, он выполняет (2), получает массив и присваивает его элементам, (3) получает длину элементов и присваивает ее len, (4) копирует массив элементов, и на этом На основе newElements присваивается длина +1, (5) добавляем элементы, которые нам нужно добавить, в newElements, (6) заменяем предыдущий массив и, наконец, переходим к (7) снятию монопольной блокировки.

После разбора исходного кода мы понимаем

  1. Как CopyOnWriteArrayList обеспечивает безопасность потоков при [записи]? Поскольку используется эксклюзивная блокировка ReentrantLock, гарантируется, что только один поток может одновременно изменять коллекцию.
  2. Данные хранятся в массиве массивов в CopyOnWriteArrayList.
  3. При добавлении элементов, вместо добавления элементов непосредственно в массив, копируется новый массив, причем длина копируемого массива равна [длина старого массива + 1], а затем старый массив заменяется новым. массивы, это особенно примечательно.

get

    public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }

Мы можем получить элемент с указанным индексом, вызвав метод get.

Сначала получить массив, а потом получить элемент с указанным индексом, вроде бы и нет проблемы, а на самом деле это проблема. Не забывайте, сейчас мы находимся в многопоточной среде разработки, иначе нет необходимости использовать вещи под JUC.

Представьте такой сценарий, когда мы получаем массив, мы держим его на ладони, как сокровище. . . Поскольку весь метод get не имеет эксклюзивной блокировки, другой поток может продолжать выполнять измененные операции, такие как операция удаления.Как и добавление, удаление также будет применяться для эксклюзивной блокировки, а также для копирования нового массива, удаления элементов и замены. Скиньте старый массив. И все эти методы get не знают, он не знает, что массив массива претерпел потрясающие изменения, и он до сих пор тупит, разглядывая массив на ладони. . . Этослабая консистенция.

Так же, как WeChat, хотя другая сторона удалила вас, но вы не знаете, вы все еще открываете окно чата с ней каждый день, готовые что-то сказать. . .

set

Мы можем изменить значение указанного элемента нижнего индекса с помощью метода set.

    public E set(int index, E element) {
        //(1)获得独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//(2)获得array
            E oldValue = get(elements, index);//(3)根据下标,获得旧的元素

            if (oldValue != element) {//(4)如果旧的元素不等于新的元素
                int len = elements.length;//(5)获得旧数组的长度
                Object[] newElements = Arrays.copyOf(elements, len);//(6)复制出新的数组
                newElements[index] = element;//(7)修改
                setArray(newElements);//(8)替换
            } else {
                //(9)为了保证volatile 语义,即使没有修改,也要替换成新的数组
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();//(10)释放独占锁
        }
    }

Когда мы вызываем метод set:

  1. Как и в случае с методом add, монопольная блокировка получается первой.Точно так же монопольную блокировку может получить только один поток, а остальные потоки будут заблокированы.
  2. Поток, который получает эксклюзивную блокировку, получает массив и присваивает его элементам.
  3. Получить старый элемент по индексу.
  4. Производится сравнение, чтобы проверить, не равен ли старый элемент новому элементу, если да, то перейти к 5-8, если нет, то перейти к 9.
  5. Получите длину старого массива.
  6. Скопируйте новый массив.
  7. Измените элемент по указанному индексу в новом массиве.
  8. Замените старый массив.
  9. Чтобы сохранить изменчивую семантику, даже если нет модификации, ее необходимо заменить новым массивом.
  10. Эксклюзивная блокировка снимается независимо от того, выполняется ли операция модификации.

Благодаря анализу исходного кода мы должны лучше понять:

  1. Эксклюзивный замок используется для обеспечения безопасности потоков [запись].
  2. Операция модификации фактически работает с копией массива и, наконец, заменяет массив.

remove

Мы можем удалить элементы с указанными координатами с помощью remove.

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

Видно, что метод удаления такой же, как и методы добавления и установки.Первым шагом является получение эксклюзивной блокировки для обеспечения потокобезопасности.Если удаляемый элемент является последним, скопируйте длину [длина старого массива] -1] новый массив, а затем замените его, чтобы последний элемент был аккуратно удален.Если удаляемый элемент не является последним, он копируется дважды, а затем заменяется.

итератор

Прежде чем анализировать исходный код, давайте рассмотрим основное использование итераторов:

public class Main {public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("copyOnWriteArrayList");
        Iterator<String>iterator=copyOnWriteArrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

результат операции:

image.png

Код очень прост, поэтому я не буду объяснять это здесь, давайте посмотрим непосредственно на исходный код итератора:

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
        static final class COWIterator<E> implements ListIterator<E> {
    
        private final Object[] snapshot;
     
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        
        // 判断是否还有下一个元素
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        
        //获取下个元素
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

Когда мы вызываем метод итератора для получения итератора, внутри будет вызываться конструктор COWIterator.Этот конструктор имеет два параметра, первый параметр — это массив массивов, а второй параметр — индекс, равный 0. Затем метод построения назначит массив массива переменной моментального снимка. Снапшот означает "моментальный снимок". Если основа Java хороша, вы должны знать, что массив является ссылочным типом, и указатель передается. Если массив изменен в другом месте, это должно быть немедленно отражено, так почему же это снимок? Что о названии? Да, если другие потоки не добавляют, не удаляют и не изменяют CopyOnWriteArrayList, то снимок представляет собой собственный массив, но если другие потоки добавляют, удаляют или изменяют CopyOnWriteArrayList, старый массив будет заменен новым массивом, но снимок по-прежнему Ссылка на исходный старый массив. То есть, когда мы используем итератор для облегчения CopyOnWriteArrayList, мы не можем гарантировать, что полученные данные являются самыми последними.слабая консистенцияпроблема.

Какие? Ты не веришь? Затем мы используем демо, чтобы доказать это:

  public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        TimeUnit.SECONDS.sleep(3);
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

результат операции:

image.png
Это не проблема, мы сначала добавляем в список точки данных, а затем открываем поток, поток для удаления некоторых элементов внутри, спит три секунды, чтобы убедиться, что поток закончил работу. Затем получите итератор через элементы, удалите элементы, которые не будут напечатаны.

Тогда ставим другую формулировку:

   public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2019");
        copyOnWriteArrayList.add("good good study");
        copyOnWriteArrayList.add("day day up");
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

На этот раз мы изменили порядок кода, сначала получив итератор, затем выполнив операцию по удалению потока и, наконец, пройдя итератор. результат операции:

image.png
Вы можете видеть, что удаленные элементы все еще печатаются.

Если мы не анализируем исходный код, не знаем принципа и не знаем слабой согласованности, когда мы используем CopyOnWriteArrayList в многопоточности, мы можем испытывать боль, хотеть разбить компьютер и не делать этого. Не знаю, почему полученные данные иногда неверны, а иногда так и есть. Следовательно, необходимо изучить принцип, будь то анализ исходного кода, чтение блогов или даже непосредственное чтение комментариев в JDK.

Среди коллекций, предоставляемых параллельными пакетами Java, CopyOnWriteArrayList должен быть самым простым.Я надеюсь, что с помощью анализа исходного кода каждый может быть уверен, что исходный исходный код JDK также может быть прочитан.