Возможная проблема параллелизма в разработке Xlet
Я занимаюсь разработкой Xlet с использованием Java 1.4 API.
Документы говорят Xlet
методы интерфейса (на самом деле это методы жизненного цикла xlet) вызываются в его специальном потоке (а не в потоке EDT). Я проверил логи - это правда. Это немного удивительно для меня, потому что это отличается от сред BB/Android, где методы жизненного цикла вызываются в EDT, но пока все нормально.
В коде проекта я вижу, что приложение широко использует Display.getInstance().callSerially(Runnable task)
звонки (это LWUIT способ запуска Runnable
на тему EDT).
Таким образом, в основном некоторые фрагменты кода внутри класса реализации Xlet выполняют операции создания / обновления / чтения над внутренними объектами состояния xlet из потока EDT, а некоторые другие фрагменты кода делают из потока жизненного цикла без какой-либо синхронизации (в том числе переменные состояния не являются объявлен как изменчивый). Что-то вроде этого:
class MyXlet implements Xlet {
Map state = new HashMap();
public void initXlet(XletContext context) throws XletStateChangeException {
state.put("foo", "bar"); // does not run on the EDT thread
Display.getInstance().callSerially(new Runnable() {
public void run() {
// runs on the EDT thread
Object foo = state.get("foo");
// branch logic depending on the got foo
}
});
}
..
}
Мой вопрос: создает ли это фон для редких проблем параллелизма? Должен ли доступ к состоянию синхронизироваться явно (или, по крайней мере, состояние должно быть объявлено как volatile)?
Я предполагаю, что это зависит от того, выполняется ли код на многоядерном процессоре или нет, потому что я знаю, что на многоядерном процессоре, если 2 потока работают на его собственном ядре, то переменные кэшируются, поэтому каждый поток имеет его собственная версия состояния, если явно не синхронизированы.
Я хотел бы получить достоверный ответ на мои вопросы.
1 ответ
Да, в описанном вами сценарии доступ к общему состоянию должен быть безопасным для потоков.
Есть 2 проблемы, о которых вам нужно знать:
Первая проблема, видимость (о которой вы уже упоминали), все еще может возникать в однопроцессорной системе. Проблема заключается в том, что JIT-компилятору разрешено кэшировать переменные в регистрах, и при переключении контекста ОС, скорее всего, будет выгружать содержимое регистров в контекст потока, чтобы впоследствии его можно было возобновить. Однако это не то же самое, что запись содержимого регистров обратно в поля объекта, поэтому после переключения контекста мы не можем предполагать, что поля объекта обновлены.
Например, возьмите следующий код:
class Example {
private int i;
public void doSomething() {
for (i = 0; i < 1000000; i ++) {
doSomeOperation(i);
}
}
}
Поскольку переменная цикла (поле экземпляра) i
не объявлен как volatile, JIT разрешено оптимизировать переменную цикла i
используя регистр процессора. Если это произойдет, то JIT не потребуется записывать значение регистра обратно в переменную экземпляра. i
пока после завершения цикла.
Итак, допустим, что поток выполняет цикл, описанный выше, и затем получает приоритет. Новый запланированный поток не сможет увидеть последнее значение i
потому что последняя ценность i
находится в регистре, и этот регистр был сохранен в локальном контексте выполнения потока. Как минимум поле экземпляра i
нужно будет объявить volatile
заставить каждое обновление i
быть видимым для других тем.
Вторая проблема - это согласованное состояние объекта. Возьмите HashMap
в вашем коде, например, внутренне он состоит из нескольких не финальных переменных-членов size
, table
, threshold
а также modCount
, куда table
это массив Entry
это формирует связанный список. Когда элемент помещается в карту или удаляется с карты, две или более из этих переменных состояния должны обновляться атомарно, чтобы состояние было согласованным. За HashMap
это должно быть сделано в течение synchronized
блок или аналогичный, чтобы он был атомным.
Что касается второго вопроса, у вас все равно будут проблемы при работе на однопроцессорном компьютере. Это связано с тем, что ОС или JVM могут превентивно переключать потоки, в то время как текущий поток частично выполняет выполнение метода put или remove, а затем переключается на другой поток, который пытается выполнить какую-то другую операцию с тем же HashMap
,
Представьте, что произойдет, если ваш поток EDT был в середине вызова метода 'get', когда происходит преимущественное переключение потока, и вы получаете обратный вызов, который пытается вставить другую запись в карту. Но на этот раз карта превышает коэффициент загрузки, что приводит к изменению размера карты и повторному хешированию и вставке всех записей.