Почему CopyOnWriteArrayList копирует при записи?

Из CopyOnWriteArrayList.java метод добавления выглядит следующим образом:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
    }

Нетрудно понять, что операция добавления должна блокироваться, меня смущает то, что она копирует старые данные в новый массив и отказывается от предыдущего. Между тем метод get выглядит следующим образом:

public E get(int index) {
        return (E)(getArray()[index]);
    }

Без блокировки в методе get. Я нахожу некоторые объяснения, некоторые говорят, что копирование в новый массив может избежать метода add и get, работающего с тем же массивом. Моя проблема в том, почему два потока не могут читать и писать одновременно?

3 ответа

Если вы просто посмотрите на верхнюю часть класса CopyOnWriteArrayList около array Ссылка может быть ответом на ваш вопрос.

 private volatile transient Object[] array; // this is volatile

return (E)(getArray()[index]);

который возвращает последнюю копию array[index] так что это threadsafe

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

getArray возвращает ссылку на array,

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

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

Чтение и запись могут происходить одновременно. Тем не менее, подразумевается, что чтения будут видеть состояние, которое скоро будет устарело, до тех пор, пока не будет установлен изменяемый набор ссылок.

Во время get(), если несколько потоков пытаются получить из списка, их проблем не будет. Потому что из-за изменчивого массива он всегда будет читать последнюю копию и возвращать элемент из массива.

Но

Во время добавления () или set() каждый раз, когда они создают новый массив, чтобы избежать проблем взаимного выполнения, это один из способов сделать потокобезопасными объекты, чтобы сделать неизменяемыми.

Если они использовали один и тот же объект массива во время добавления или установки, то они должны выполнить синхронизированный обход. Или это может вызвать исключение, если какой-либо поток добавляет / удаляет объект в списке во время обхода.

Согласно документу Java

Потокобезопасный вариант java.util.ArrayList, в котором все мутативные операции (добавление, установка и т. Д.) Реализованы путем создания новой копии базового массива.

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

Видеть это

package com.concurrent;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list=new CopyOnWriteArrayList<>();


        Viewer viewer=new Viewer();
        viewer.setList(list);       
        Thread t1=new Thread(viewer);

        Adder adder=new Adder();
        adder.setList(list);

        Thread t=new Thread(adder);
        t.start();
        t1.start();

    }

    static class Adder implements Runnable{

        private List<Integer> list;
        public void setList(List<Integer> list) {
            this.list = list;
        }
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                list.add(i);
                System.out.println("Added-"+i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }

    }

    static class Viewer implements Runnable{

        private List<Integer> list;
        public void setList(List<Integer> list) {
            this.list = list;
        }
        @Override
        public void run() {
            while (true) {
                System.out.println("Length of list->"+list.size());
                for (Integer i : list) {
                    System.out.println("Reading-"+i);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        }

    }
}
Другие вопросы по тегам