Есть ли пример реализации алгоритма Петерсона для взаимного исключения в Java?
Алгоритм Петерсона в Java?
Ответы (5)
Здесь никто не предоставил правильную/безопасную реализацию этого алгоритма на Java. Я не уверен, как должно работать решение Джона В., поскольку в нем отсутствуют части (а именно объявления ThreadLocals и объяснение того, что должно быть в его массиве — примитив booleans
не имеет get()
и set()
).
Глава 17 Спецификации языка Java объясняет Модель памяти Java. Особый интерес представляет Раздел 17.4.5, который описывает порядок происходит до. Об этом довольно легко думать в рамках одного потока. Рассмотрим фрагмент:
int x, y, z, w;
x = 0;
y = 5;
z = x;
w = y;
Все согласятся, что в конце этого фрагмента x
и z
равны 0
, а y
и w
равны 5
. Игнорируя объявления, у нас есть шесть действий:
- Написать
x
- Пишите
y
- Чтение от
x
- Пишите
z
- Чтение от
y
- Пишите
w
Поскольку все они появляются в одном потоке, JLS говорит, что эти операции чтения и записи гарантированно демонстрируют этот порядок: каждое действие n выше (поскольку действия находятся в одном потоке) имеет событие «происходит до». связь со всеми действиями m, m > n.
Но как насчет разных потоков? При обычном доступе к полям между потоками не устанавливаются отношения "происходит до". Это означает, что поток A может увеличивать общую переменную, а поток B может читать эту переменную, но не видеть новое значение< /эм>. При выполнении программы в JVM распространение записи потока A могло быть переупорядочено так, чтобы оно происходило после чтения потока B.
На самом деле, поток A может записывать в переменную x
, а затем в переменную y
, устанавливая связь между этими двумя действиями в потоке A. Но поток B может читать x
и y
, и B имеет право получить новое значение y
до появления нового значения x
. Спецификация говорит:
Более конкретно, если два действия имеют общую связь «происходит до», они не обязательно должны казаться произошедшими в этом порядке для любого кода, с которым они не имеют общей связи «произошло до».
Как это исправить? Для обычного доступа к полям достаточно ключевого слова volatile
:
Запись в изменчивую переменную (§8.3.1.4) v синхронизируется со всеми последующими операциями чтения v любым потоком (где последующее определяется в соответствии с порядком синхронизации).
синхронизируется с является более сильным условием, чем "происходит-до", и, поскольку "происходит-до" является транзитивным, если поток A хочет, чтобы поток B видел свои записи в x
и y
, ему просто нужно записать в volatile переменная z
после записи x
и y
. Поток B должен прочитать из z
перед чтением x
и y
, и он гарантированно увидит новые значения x
и y
.
В решении Габриэля мы видим такую закономерность: запись происходит в in
, которая не будет видна другим потокам, но затем происходит запись в turn
, так что другие потоки гарантированно увидят обе записи, пока они читают turn
первыми.
К сожалению, условное выражение цикла while является обратным: чтобы гарантировать, что поток не увидит устаревшие данные для in
, цикл while должен сначала прочитать из turn
:
// ...
while (turn == other() && in[other()]) {
// ...
Имея в виду это исправление, большая часть остального решения в порядке: в критической секции нас не волнует устаревание данных, потому что мы находимся в критической секции! Единственный другой недостаток возникает в конце: Runnable устанавливает in[id]
в новое значение и завершает работу. Будет ли гарантировано, что другой поток увидит новое значение in[id]
? Спецификация говорит нет:
Последнее действие в потоке T1 синхронизируется с любым действием в другом потоке T2, обнаружившем завершение T1. T2 может выполнить это, вызвав T1.isAlive() или T1.join().
Итак, как мы это исправим? Просто добавьте еще одну запись в turn
в конце метода:
// ...
in[id] = false;
turn = other();
}
// ...
Поскольку мы переупорядочили цикл while, другой поток гарантированно увидит новое ложное значение in[id]
, потому что запись в in[id]
происходит до того, как произойдет запись в turn
, до того, как произойдет чтение из turn
, до того, как произойдет чтение из in[id]
.
Излишне говорить, что без метрики тонны комментариев этот метод хрупок, и кто-то может прийти и что-то изменить и тонко нарушить правильность. Простого объявления массива volatile
недостаточно: как объясняется в этом поток Билла Пью (одного из ведущих исследователей модель памяти Java), объявление массива volatile
делает обновления массива reference видимыми для других потоков. Обновления элементов массива не обязательно видны (отсюда и все циклы, через которые нам только что пришлось пройти, используя другую переменную volatile
для защиты доступа к элементам массива).
Если вы хотите, чтобы ваш код был ясным и лаконичным, оставьте его таким, какой он есть, и измените in
на AtomicIntegerArray (используйте 0 для false, 1 для true; AtomicBooleanArray не существует). Этот класс действует как массив, все элементы которого равны volatile
, и поэтому прекрасно решит все наши проблемы. В качестве альтернативы вы можете просто объявить две изменчивые переменные, boolean in0
и boolean in1
, и обновить их вместо использования логического массива.
Если у вас нет особой потребности в агоритме Петерсона (что было бы странно при работе с языком высокого уровня, таким как Java), я предлагаю вам взглянуть на средства синхронизации, встроенные в язык.
Например, вам может пригодиться эта глава книги, посвященная условиям гонки и взаимному исключению в Java: http://java.sun.com/developer/Books/performance2/chap3.pdf
В частности:
Java предоставляет встроенную поддержку ожидания этого «изменения состояния» через понятие условия. Однако термин «условие» является немного неправильным, поскольку от пользователя полностью зависит, возникло ли условие на самом деле. Кроме того, условие не обязательно должно быть конкретно истинным или ложным. Чтобы использовать условия, необходимо ознакомиться с тремя ключевыми методами класса Object:
• wait(): этот метод используется для ожидания условия. Он вызывается, когда в настоящее время удерживается блокировка для определенного (общего) объекта.
• notify(): этот метод используется для уведомления отдельного потока о том, что условие (возможно) изменилось. Опять же, этот метод вызывается, когда в данный момент удерживается блокировка для определенного объекта. В результате этого вызова может быть пробужден только один поток.
• notifyAll(): этот метод используется для уведомления нескольких потоков о том, что условие (возможно) изменилось. Все потоки, работающие во время вызова этого метода, будут уведомлены.
Сам я не смог найти его в сети, поэтому решил попробовать написать:
public class Peterson implements Runnable {
private static boolean[] in = { false, false };
private static volatile int turn = -1;
public static void main(String[] args) {
new Thread(new Peterson(0), "Thread - 0").start();
new Thread(new Peterson(1), "Thread - 1").start();
}
private final int id;
public Peterson(int i) {
id = i;
}
private int other() {
return id == 0 ? 1 : 0;
}
@Override
public void run() {
in[id] = true;
turn = other();
while (in[other()] && turn == other()) {
System.out.println("[" + id + "] - Waiting...");
}
System.out.println("[" + id + "] - Working ("
+ ((!in[other()]) ? "other done" : "my turn") + ")");
in[id] = false;
}
}
Не стесняйтесь комментировать, это будет оценено :)
Вы действительно должны проверить книгу Искусство многопроцессорного программирования. Он очень подробно рассматривает различные реализации блокировок (как спиновых, так и блокирующих). Он также рассматривает различные другие типы параллельных алгоритмов (например, список пропуска). Вот фрагмент из его книги об алгоритме блокировки Петерсона.
Class Peterson implements Lock{
private final boolean []flag = new boolean[2];
private volatile int victim;
public void lock(){
int i = ThreadID.get(); // ThreadID is a ThreadLocal field
int j= 1 - i;
flag[i] = true; // I'm Interested
victim = i; // You go first
while(flag[j] && victim == i){}; //wait
}
public void unlock(){
int i = ThreadID.get();
flag[i] = false; //Not interested anymore
}
}
Хотя это и не алгоритм Патерсона, классы AtomicBoolean и Atomic* используют подход незаблокированных циклов занятости для обновления общих данных. Они могут соответствовать вашим требованиям.