По материалам Read/Write Locks in Java
Условия:
Имеются общие данные, к которым потоки обращаются на чтение и изменение (запись). При этом изменение данных происходит относительно редко. Гораздо чаще происходит их чтение.
Несколько потоков, которые читают данные, не изменяя их, не создают проблем друг другу. Поэтому нет смысла их синхронизировать - ведь они не мешают друг другу. Но если хотя бы один поток начал изменение данных, то все другие потоки (читающие и изменяющие данные) должны подождать, пока этот поток не закончит работу. Т.е. мы можем иметь множественных одновременных читателей данных, но лишь один поток, который меняет данные.
Задача:
Требуется эффективно синхронизировать читающие и изменяющие потоки между собой.
Решение:
Для начала, еще раз подытожим условия блокировки:
Доступ для чтения: если нет потоков, которые изменяют данные, а также нет потоков, которые запросили доступ для изменения данных.
Доступ для записи: если нет потоков, которые читают или изменяют данные.
Если поток хочет прочитать данные, то это позволяется, если ни один поток не изменяет данные и ни один поток не запросил доступ на запись данных. Мы отдаем записи данных более высокий приоритет, чем чтению. Если бы мы так не делали, то могло бы возникнуть "голодание" (starvation) потоков, изменяющих данные (при условии, что запросы на чтение данных происходят чаще, чем запросы на запись данных). Потоки, запрашивающие доступ на запись, должны были бы ждать, пока не отработают потоки, читающие данные. Так как запросов на чтение много, то новые читающие потоки добавлялись бы постоянно, что привело бы в долговременному (а то и вообще бесконечному) простою потоков на запись. Поэтому поток может получить разрешение на чтение, только если ни один поток не изменяет данные и ни один поток не запросил доступ на запись.
Поток, который хочет получить доступ к записи данных, получает этот доступ, если только ни один поток не читает и не пишет данные. Приэтом не имеет значения, сколько поток запросило доступ на запись, так как мы хотим обеспечить равные шансы всем этим потокам.
Поэтому мы можем написать следующие код:
public class ReadWriteLock{
private int readers = 0;
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead() throws InterruptedException{
while(writers > 0 || writeRequests > 0){
wait();
}
readers++;
}
public synchronized void unlockRead(){
readers--;
notifyAll();
}
public synchronized void lockWrite() throws InterruptedException{
writeRequests++;
while(readers > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite() {
writers--;
notifyAll();
}
}
Здесь мы имеем два метода для начала блокировки, и два метода для окончания блокировки. Код, который мы реализовали, не подразумевает возвратного блокирования (reentrant locking).
Ниже приведен пример использования блокировки чтения/записи для потоко-безопасного кеша:
public class Cache {
HashMap<Integer,String> map = new HashMap<Integer,String>();
ReadWriteLock lock = new ReadWriteLock();
public String get(Integer key) throws CacheException {
try {
lock.lockRead();
return map.get(key);
} catch (InterruptedException ie) {
throw new CacheException(ie);
} finally {
lock.unlockRead();
}
}
public void put(Integer key, String value) throws CacheException {
try {
lock.lockWrite();
map.put(key,value);
} catch (InterruptedException ie) {
throw new CacheException(ie);
} finally {
lock.unlockWrite();
}
}
public void remove(Integer key) throws CacheException {
try {
lock.lockWrite();
map.remove(key);
} catch (InterruptedException ie) {
throw new CacheException(ie);
} finally {
lock.unlockWrite();
}
}
}