Можно ли заблокировать отдельные элементы по индексу в ArrayList в Java? - PullRequest
0 голосов
/ 07 февраля 2020

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

class Program() {

    List<Object> list = new ArrayList<Object>();

    readFromList(int index) {
        synchronized(list.get(index)) {   //If i exclude this lock can other threads access the element 
            Object tmp = list.get(index); //if the lock in writeToList() is active?
            return tmp;
        }
    }

    writeToList(int index, Object obj) {
        synchronized(list.get(index)) {
            list.set(obj);
        }
    }

Так было бы возможно сохранять блокировки на индекс для ArrayList, и когда разные потоки хотят получить доступ к объектам в списке, им нужно будет только ожидать блокировки для определенных c элементов в массиве?

1 Ответ

4 голосов
/ 07 февраля 2020

synchronized не работает с переменными, но объекты . Он также не блокирует память, а только обеспечивает взаимное исключение и гарантии видимости памяти для потоков, выполняющих synchronized на одном и том же объекте .

Это означает, что любая синхронизация с изменяемой переменной не не работает вообще.

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

Затем возникают общие проблемы с синхронизацией с изменяемой переменной.

Код типа

synchronized(variable) {
    return variable;
}

содержит две операции чтения variable, причем первая выполняется до входа в блок synchronized, чтобы определить, на каком объекте синхронизироваться. Так как это первое чтение выполняется без какого-либо примитива синхронизации, не гарантируется, что вы увидите самое последнее значение. Хотя «последнее» в любом случае не имеет большого значения, так как авторы, выполняющие

synchronized(variable) {
    variable = newValue;
}

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

Но даже наиболее тривиальный сценарий: один писатель, выполняющий synchronized(variable) { variable = newValue; }, за которым следует один читатель, выполняющий synchronized(variable) { return variable; }, не работает, поскольку писатель синхронизировал со старым значением , в то время как считыватель может прочитать любое значение в первом доступе, включая самое последнее значение . Поскольку оба потока синхронизируются на разных объектах, нет никаких гарантий, что делает второе чтение читателя неопределенным. Как бы интуитивно это ни было, второе чтение может даже прочитать старое значение.

Но это не худший результат. Когда вы не используете неизменяемые объекты, такие как String или Integer, это редкое чтение может возвращать более свежую ссылку на объект, но не гарантирует видимость его полей-членов, что приводит к совершенно несовместимому состоянию.

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

Когда у вас все в порядке с фиксированным размером, который используется в вашем примере только List.set и List.get означает, что вы можете использовать AtomicReferenceArray:

public class Program {
     // provide needed size
    final AtomicReferenceArray<Object> data = new AtomicReferenceArray<>(100);

    Object readFromList(int index) {
        return data.get(index);
    }

    // returns old object
    Object writeToList(int index, Object obj) {
        return data.getAndSet(index, obj);
    }
}

Если вам нужен изменяемый размер или вам действительно нужно заблокировать место для нетривиальной операции, вы можете использовать, например,

public class Program {
    final ConcurrentHashMap<Integer, Object> data = new ConcurrentHashMap<>();

    // returns the new index
    int addToList(Object o) {
        int ix;
        do ix = data.size(); while(data.putIfAbsent(ix, o) != null);
        return ix;
    }

    Object readFromList(int index) {
        return data.get(index);
    }

    void updateList(int index, Object input) {
        data.compute(index, (ix, old) -> {
            // compute based on old and input
            return input;// or computed value
        });
    }
}
...