Я новичок в многопоточности и хочу понять это лучше. Теперь я хочу узнать, как сделать массив потокобезопасным в 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?
Неизменяемые структуры данных всегда потокобезопасны.
Я удалил свой ответ. Я сказал «не потокобезопасный», потому что мне показалось, что класс допускает небезопасный способ его использования. Но есть ли что-нибудь разумное, что вы могли бы сделать, чтобы предотвратить небезопасное использование? Внезапно я не так уверен. Ничто не является полностью надежным. Они всегда изобретают дураков получше.
Ваше понимание правильное. Экземпляр 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 для добавления/удаления записей в таблице, не передавая всю таблицу процессу для ее обновления....
Спасибо за ваш ответ! Что касается вашего вопроса, я изменил код, чтобы прояснить его и подчеркнуть свой вопрос. Код не обязательно имеет смысл, поскольку я упустил некоторую логику внутри синхронизированных блоков.
Спецификация языка 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();
}
}
Это не имеет ничего общего с созданием потокобезопасного массива (что фактически невозможно). Показанный код предназначен для замены объекта (который является массивом) потокобезопасным способом. Показанный код является потокобезопасным, и операция чтения этого массива будет потокобезопасной при условии, что он не будет изменен (как в отдельных записях массива, получающих новое значение) другими потоками после вызова
updateData
, и это потому что массивы сами по себе не являются потокобезопасными и не могут быть поточно-безопасными (операции всегда требуют некоторой формы взаимного исключения, чтобы быть потокобезопасными).