Для конкретной потокобезопасной структуры данных мне необходимо защитить доступ к центральной структуре данных (а именно к массиву байтов). Я предпочитаю использовать ReentrantLocks в этом случае из-за его политики справедливости, а также расширенных возможностей с созданием нескольких условий.
Условия параллелизма сложны и перечислены ниже:
- Центральный массив байтов должен быть защищен исключительно (т.е. один поток одновременно).
- Два метода доступа (foo и bar) должны работать одновременно (внутренняя блокировка, если они попытаются получить доступ к центральному массиву байтов).
- Вызовы любого метода (foo и bar) должны быть эксклюзивными (т. е. множественные вызовы foo из разных потоков приведут к блокировке одного потока).
В моей первоначальной реализации я решил реализовать две вложенные блокировки, как показано ниже:
ReentrantLock lockFoo = new ReentrantLock(true);
ReentrantLock lockCentral = new ReentrantLock(true);
Condition centralCondition = lockCentral.newCondition();
public void foo(){
// thread-safe processing code here
lockFoo.lock();
lockCentral.lock();
try{
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
lockCentral.unlock();
lockFoo.unlock();
}
}
структура аналогична методу bar
, просто с другим объектом блокировки lockBar
. Кроме того, код для простоты сократил мои более сложные ожидания и сигналы с несколькими условиями до одного условия.
Используя это, я не могу не чувствовать, что код кажется излишне сложным и неясным не только потому, что есть две вложенные блокировки, они разделяют одну попытку-наконец, не говоря уже о том, как lockCentral
может быть освобожден и повторно получен несколько раз, в то время как lockFoo
проводится на протяжении.
Вместо этого я попытался реорганизовать внешний замок (lockFoo
и lockBar
) как условие lockCentral
, как показано ниже:
ReentrantLock lockCentral = new ReentrantLock(true);
Condition fooCondition = lockCentral.newCondition();
Condition centralCondition = lockCentral.newCondition();
boolean isInFoo = false;
public void foo(){
// thread-safe processing code here
lockCentral.lock();
try{
// implement method exclusiveness via fooCondition
try{
while(isInFoo){
fooCondition.await();
}
isInFoo = true;
}catch(InterruptedException ex){
return;
}
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
isInFoo = false;
fooCondition.signal();
lockCentral.unlock();
}
}
после некоторого осмотра я не могу решить, лучше ли первое или второе (особенно с включением этого случайного логического значения). Идея упрощения кода, по-видимому, привела к созданию более длинного кода, что в данном случае очень нелогично.
Есть ли какое-то соглашение или веская причина, чтобы аргументировать:
Использование одной блокировки для каждого контекста блокировки (прежний код, где разные причины блокировки используют разные блокировки).
Использование одной блокировки для каждого ресурса блокировки (последний код, где защищаемая центральная структура использует одну блокировку, а все остальное реализовано как условия для доступа к указанной структуре).