Как сделать массив потокобезопасным в Java?

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

Мой подход был следующим: У меня есть класс данных, содержащий массив и два метода updateData(Integer[] data) и getUpdate() для доступа к массиву и управления им.

public class  Data <T extends Comparable> {
    private T[] data;

    public T[] getUpdate() {
        synchronized (this) {
            return data.clone();
        }
    }

    public void updateData(T[] data) {
        synchronized (this) {
            this.data = data.clone();
        }
    }
}

Этот объект данных теперь используется в двух разных потоках: основном потоке, который обновляет данные, и потоке Swing EDT, который считывает данные и использует их.

public static void main(String[] args) {
    Data<Integer> data = new Data<>();
    data.updateData(new Integer[]{1,2,3,4,5,6,7,8,10});


    SwingUtilities.invokeLater(() -> {
        ColumnVisualizer visualizer = new ColumnVisualizer();

        new Timer(100, e -> {
            visualizer.setData(data.getUpdate());
        }).start();
    });
    
    while(condition) {
        data.updateData(createNewData());

    }
}

Насколько я понимаю, если я обращаюсь к массиву только через методы updateData() и getUpdate(), которые я сделал потокобезопасными с помощью ключевого слова Synchronized, сами данные являются потокобезопасными. Поскольку, если поток вызывает метод getUpdate(), он устанавливает блокировку экземпляра Data, а когда другой поток хочет вызвать updateData(), он не может получить доступ к той части, где старый массив заменяется новым, пока экземпляр Data не будет больше не смотрел.

Правильно ли я понимаю, и является ли это удобным способом сделать массив потокобезопасным в Java?

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

Mark Rotteveel 21.06.2024 12:27

Неизменяемые структуры данных всегда потокобезопасны.

chris576 21.06.2024 13:00

Я удалил свой ответ. Я сказал «не потокобезопасный», потому что мне показалось, что класс допускает небезопасный способ его использования. Но есть ли что-нибудь разумное, что вы могли бы сделать, чтобы предотвратить небезопасное использование? Внезапно я не так уверен. Ничто не является полностью надежным. Они всегда изобретают дураков получше.

Solomon Slow 23.06.2024 05:48
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
3
128
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Ваше понимание правильное. Экземпляр Data будет заблокирован до тех пор, пока синхронизированная блокировка не будет завершена одним из методов (getUpdate/updateData).

Хотя, основываясь на вашем коде, у меня есть некоторые наблюдения, которые могут быть вам полезны. Предположим, что в момент времени time1 приходит поток, назовем его th1, вызывает getUpdate и извлекает клон данных. Предположим, что через миллисекунду (или раньше) другой поток, назовем I th2, получит клонированные данные из того же метода getUpdate. На данный момент и th1, и th2 имеют одинаковые записи в таблице данных. Предполагая, что через 4 миллисекунды процесс th1 обновляет данные с помощью updateData, а через 1 миллисекунду процесс th2 вызывает тот же метод.

Давайте посмотрим, что произошло с этими двумя процессами: сразу после вызова updateData th2 таблица данных будет содержать все, что туда поместил th2, полностью игнорируя то, что th1 поместил туда 1 миллисекунду назад (процесс th1 как будто никогда не происходил). Это то, чего ты хочешь ? Я не говорю, что это неправильно, я просто говорю, что это произойдет, если два процесса будут использовать таблицу практически одновременно (с разницей в 1 мс или меньше).

Если вы хотите, чтобы в конце th2 таблица данных содержала информацию как th1, так и th2, вам, вероятно, понадобится другой подход. Например. заблокируйте таблицу для процесса или используйте методы класса Data для добавления/удаления записей в таблице, не передавая всю таблицу процессу для ее обновления....

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

KaMuench 22.06.2024 22:44

Спецификация языка Java пишет:

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

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

Локальные переменные (§14.4), параметры формального метода (§8.4.1) и параметры обработчика исключений (§14.20) никогда не передаются между потоками и на них не влияет модель памяти.

Два доступа к одной и той же переменной (чтение или запись) называются конфликтными, если хотя бы один из них является записью.

а затем подробно описывает, что требуется некоторая синхронизация между конфликтующими доступами в разных потоках.

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

Первой такой переменной является поле data, которое содержит ссылку на массив. Легко заметить, что data читается и пишется разными потоками. Поэтому требуется какое-то действие по синхронизации. Поскольку доступ к переменной всегда осуществляется в синхронизированном блоке (на одном и том же объекте), мы знаем, что монитор сначала разблокируется, а затем блокируется между конфликтующими обращениями, и поскольку

действие разблокировки на мониторе m синхронизируется со всеми последующими действиями блокировки на m (где «последующие» определяются в соответствии с порядком синхронизации).

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

Хорошо, а как насчет элементов массива? Есть ли конфликтующие доступы в отдельных потоках? Обратите внимание, что на самом деле у нас есть три разных массива: исходный массив, передаваемый в updateData(...), копия, созданная внутри updateData(...), и копия, созданная внутри getData(). Из них только ко второму обращаются отдельные потоки, причем доступы конфликтные, так как мы записываем массив, а затем читаем его в другом потоке. Однако мы можем применить те же рассуждения, что и для data, чтобы сделать вывод, что между этими доступами произошло действие синхронизации, и поэтому доступ синхронизирован правильно.

Наконец, мы обращаем внимание на поля объекта T, которые были переданы из одного потока в другой. Мы не знаем, как осуществляется доступ к этим полям, и поэтому не можем с уверенностью сделать вывод, правильно ли синхронизирован код. Например, если поток записи должен был записать некоторые поля T после того, как ссылка на объект T была передана другому потоку, у нас может возникнуть конфликтный доступ без синхронизации, что приведет к неправильной синхронизации программы. Однако, если мы знаем, что поток записи не изменяет объекты T после того, как поделился ими с другим потоком, мы знаем (из приведенных выше рассуждений), что передача включает в себя действие синхронизации, то есть потоки синхронизируются между записью и чтением. , и поэтому мы можем полагаться на то, что запись произошла до (и была видна) чтения.

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

public class  Data <T extends Comparable> {
    private T[] data;

    public T[] getUpdate() {
        T[] d;
        synchronized (this) {
            d = this.data;
        }
        return d.clone();
    }

    public void updateData(T[] data) {
        var copy = data.clone();
        synchronized (this) {
            this.data = copy;
        }
    }
}

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

Фактически, безопасное совместное использование ссылки на объект с другим потоком является настолько распространенной практикой, что Java API предлагает для этой цели класс, который позволит нам еще больше упростить ваш код:

public class  Data <T extends Comparable> {
    private final AtomicReference<T[]> data;

    public T[] getUpdate() {
        return data.get().clone();
    }

    public void updateData(T[] data) {
        this.data.set(data.clone());
    }
}

или же мы могли бы просто сделать ссылку volatile, потому что

Запись в изменчивую переменную v (§8.3.1.4) синхронизируется со всеми последующими чтениями v любым потоком (где «последующее» определяется в соответствии с порядком синхронизации).

public class  Data <T extends Comparable> {
    private volatile T[] data;

    public T[] getUpdate() {
        return data.clone();
    }

    public void updateData(T[] data) {
        this.data = data.clone();
    }
}

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