Общее·количество·просмотров·страницы

Java Dev Notes - разработка на Java (а также на JavaScript/Python/Flex и др), факты, события из АйТи

Показаны сообщения с ярлыком concurrency patterns. Показать все сообщения
Показаны сообщения с ярлыком concurrency patterns. Показать все сообщения

пятница, 14 марта 2014 г.

Элементарная реализация BlockingQueue на Java

BlockingQueue (блокирующая очередь) - это очередь, которая блокирует поток, в двух случаях:
  • поток пытается получить элементы из пустой очереди
  • поток пытается положить элементы в полную очередь

Когда поток пытается получить элементы из пустой очереди, он ставится в ожидание до тех пор, пока какой-нибудь другой поток не положит элементы в очередь. Аналогично, когда поток пытается положить элементы в полную очередь, он ставится в ожидание до тех пор, пока какой-нибудь другой поток не возьмет элементы их очереди и таким образом не освободит место в ней. Естественно, введение понятия "полная очередь" подразумевает, что очередь имеет ограниченный размер, который обычно задается в конструкторе.

В Java есть интерфейс BlockingQueue, а также множество реализаций этого интерфейса:

Тем не менее, было бы полезно самим реализовать простейшую блокирующую очередь для понимания ее работы. Причем наша реализация (для начала) должна быть самой простой. Реализации в JDK намного более сложные, чем наша примитивная реализация, которая приведена ниже. Затем можно было бы детально разобрать реализации блокирующих очередей в Java. Впрочем, разбор реализаций в Java мы оставим когда-нибудь на потом, а сейчас займемся собственной простейшей реализацией блокирующей очереди.

public class BlockingQueue {

    private List queue = new LinkedList();
    private int  limit = 10;

    public BlockingQueue(int limit){
        this.limit = limit;
    }


    public synchronized void put(Object item) throws InterruptedException  {
        while (this.queue.size() == this.limit) {
            wait();
        }
        if (this.queue.size() == 0) {
            notifyAll();
        }
        this.queue.add(item);
    }


    public synchronized Object take() throws InterruptedException{
        while (this.queue.size() == 0){
            wait();
        }
        if (this.queue.size() == this.limit){
            notifyAll();
        }

        return this.queue.remove(0);
    }
}

Рассмотрим метод put:

public synchronized void put(Object item) throws InterruptedException  {
    while (this.queue.size() == this.limit) {
        wait();
    }
    if (this.queue.size() == 0) {
        notifyAll();
    }
    this.queue.add(item);
}
Если очередь заполнена, то поток ставится в ожидание. Если же очередь пустая, то вызывается метод notifyAll, который пробуждает потоки, которые поставлены в ожидание при получении элементов из очереди. Аналогично работает метод take.

Обратите внимание, что в нашей простейшей реализации методы put, take являются synchronized в отличие от реализаций в JDK. Это сделано из двух соображений:

  • простота
  • желание обойтись только элементарными примитивами синхронизации в JDK, а именно методами wait, notifyAll

Давайте промоделируем работу очереди размеров в 5 элементов. Пусть у нас есть один поток (Producer), который, скажем, каждые 2 миллисекунды создает и кладет новый объект в очередь (пока что не подключаем поток, который забирает элементы из очереди). Очевидно, что через 10 миллисекунд, после того как очередь вся будет заполнена, при попытке положить шестой по счету элемент в очередь, этот поток будет поставлен в ожидание вот этим кусом кода из метода put:

while (this.queue.size() == this.limit) {
    wait();
}
Теперь подключим второй поток, который будет забирать элементы из очереди (Consumer). Он определит, что очередь полная и пошлет notification всем ожидающим потокам. Это произойдет в следующем куске кода метода take:
if (this.queue.size() == this.limit){
    notifyAll();
}
После этого поток заберет первый элемент из списка, и вернет его в методе take, выйдя таким образом из этого (synchronized!!!) метода и освободив монитор, ассоциированный c очередью. Затем монитор захватит поток Producer, пробудившийся от ожидания, и продолжит класть элементы в очередь.

Теперь приведем полный код демо-примера:

import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {

    private List queue = new LinkedList();
    private int  limit = 10;

    public BlockingQueue(int limit){
        this.limit = limit;
    }


    public synchronized void put(T item) throws InterruptedException  {
        System.out.println("[BlockingQueue] try to put: " + item );
        while (this.queue.size() == this.limit) {
            System.out.println("[BlockingQueue] queue is full, waiting until space is free");
            wait();
        }
        if (this.queue.size() == 0) {
            System.out.println("[BlockingQueue] queue is empty, notify");
            notifyAll();
        }
        this.queue.add(item);
        System.out.println("[BlockingQueue] put ok: " + item );
    }


    public synchronized T take() throws InterruptedException{
        System.out.println("[BlockingQueue] try to take");
        while (this.queue.size() == 0){
            System.out.println("[BlockingQueue] queue is empty, waiting until smth is put");
            wait();
        }
        if (this.queue.size() == this.limit){
            System.out.println("[BlockingQueue] queue is full, notify");
            notifyAll();
        }

        T item = this.queue.remove(0);
        System.out.println("[BlockingQueue] take ok: " + item );
        return item;
    }
}
import java.util.Random;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new BlockingQueue(5);
        new Thread(new Producer(queue)).start();
        Thread.currentThread().sleep(1000);
        new Thread(new Consumer(queue)).start();
    }

    static class Producer implements Runnable {
        private final BlockingQueue queue;

        public Producer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            System.out.println("[Producer] run");
            while (true) {
                try {
                    queue.put(produce());
                    Thread.currentThread().sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private Integer produce() {
            Integer i = new Random().nextInt(100);
            System.out.println("[Producer] produce: " + i);
            return i;
        }
    }

    static class Consumer implements Runnable {
        private final BlockingQueue queue;

        public Consumer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            System.out.println("[Consumer] run");
            while (true) {
                try {
                    consume();
                    Thread.currentThread().sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void consume() throws InterruptedException {
            Integer i = queue.take();
            System.out.println("[Consumer] consumed: " + i);
        }
    }
}

Вывод консоли будет такой (первая часть, где Producer кладет первые пять элементов в очередь):
[Producer] run
[Producer] produce: 86
[BlockingQueue] try to put: 86
[BlockingQueue] queue is empty, notify
[BlockingQueue] put ok: 86
[Producer] produce: 73
[BlockingQueue] try to put: 73
[BlockingQueue] put ok: 73
[Producer] produce: 20
[BlockingQueue] try to put: 20
[BlockingQueue] put ok: 20
[Producer] produce: 84
[BlockingQueue] try to put: 84
[BlockingQueue] put ok: 84
[Producer] produce: 73
[BlockingQueue] try to put: 73
[BlockingQueue] put ok: 73
[Producer] produce: 67
[BlockingQueue] try to put: 67
[BlockingQueue] queue is full, waiting until space is free
Затем вступает в работу Consumer:
[Consumer] run
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 86
[Consumer] consumed: 86
[BlockingQueue] put ok: 67
[Producer] produce: 63
[BlockingQueue] try to put: 63
[BlockingQueue] queue is full, waiting until space is free
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 73
[Consumer] consumed: 73
[BlockingQueue] put ok: 63
[Producer] produce: 90
[BlockingQueue] try to put: 90
[BlockingQueue] queue is full, waiting until space is free
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 20
[Consumer] consumed: 20
[BlockingQueue] put ok: 90
[Producer] produce: 57
[BlockingQueue] try to put: 57
[BlockingQueue] queue is full, waiting until space is free
На этом куске вывода хорошо видно, как Consumer и Producer попеременно используют очередь для обмена данными.

пятница, 17 сентября 2010 г.

Пример блокировки чтения/записи. Потокобезопасное префиксное дерево (threadsafe trie)

Рассмотрим применение блокировки чтения/записи. Реализуем потокобезопасное префиксное дерево. Блокировки чтения/записи рассматриваются в данных постах:
Блокировка чтения/записи. Часть 1.
Блокировка чтения/записи. Часть 2
Блокировка чтения/записи. Часть 3
Блокировка чтения/записи. Часть 4
Блокировка чтения/записи. Часть 5
Блокировка чтения/записи. Часть 6
Префиксное дерево рассматривается в этом посте:
Префиксное дерево (trie)

Вот код потокобезопасного префиксного дерева:

import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReadWriteLock;
 
public class ThreadsafeTrie {
ReadWriteLock rwlLock = new ReentrantReadWriteLock();
 
static class TrieNode {
Map<Character, TrieNode> children = new TreeMap<Character, TrieNode>();
boolean leaf;
}
 
TrieNode root = new TrieNode();
 
public void put(String s) {
TrieNode v = root;
 
rwlLock.writeLock().lock();
{
for (char ch : s.toLowerCase().toCharArray()) {
if (!v.children.containsKey(ch)) {
v.children.put(ch, new TrieNode());
}
v = v.children.get(ch);
}
v.leaf = true;
}
rwlLock.writeLock().unlock();
}
 
public boolean find(String s) {
TrieNode v = root;
boolean result = true;
 
rwlLock.readLock().lock();
{
for (char ch : s.toLowerCase().toCharArray()) {
if (!v.children.containsKey(ch)) {
result = false;
break;
} else {
v = v.children.get(ch);
}
}
}
rwlLock.readLock().unlock();
return result;
}
 
static Map<Integer,String> levelSpacesMap = new ConcurrentHashMap<Integer,String>();
 
static String getSpace(int level) {
String result = levelSpacesMap.get(level);
if (result == null) {
StringBuilder sb = new StringBuilder();
for (int i=0; i<level; i++) {
sb.append(" ");
}
result = sb.toString();
levelSpacesMap.put(level,result);
}
return result;
}
 
public void printSorted() {
rwlLock.readLock().lock();
{
printSorted2(root,0);
}
rwlLock.readLock().unlock();
}
 
private void printSorted2(TrieNode node, int level) {
for (Character ch : node.children.keySet()) {
System.out.println(getSpace(level)+ch);
printSorted2(node.children.get(ch), level+1);
}
if (node.leaf) {
System.out.println();
}
}
 
// Usage example
public static void main(String[] args) {
ThreadsafeTrie trie = new ThreadsafeTrie();
trie.put("hello");
trie.put("house");
trie.put("hell");
trie.put("world");
System.out.println(trie.find("hello"));
trie.printSorted();
}
}


Здесь мы используем ReadWriteLock из пакета java.util.concurrent.locks. Рассмотрим операции над деревом.

Операция put. Тут происходит изменение данных, поэтому нам здесь нужно ставить блокировку на запись.
Операции find, printSorted. Здесь происходит лишь чтение данных, поэтому нам здесь достаточно ставить блокировку на чтение.

Использование блокировки чтения/записи делает работу класса более быстрой, чем если бы мы поставили перед каждым методом атрибут synchronized, и таким бы образом блокировали все операции (и чтения, и записи), давая в каждый момент времени выполняться лишь одному потоку.

вторник, 14 сентября 2010 г.

Блокировка чтения/записи. Часть 6

Итак, приведем полностью код класса ReadWriteLock, который поддерживает все варианты повторных блокировок:


Код класса:

public class ReadWriteLock {
private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
private int writers = 0;
private int writeRequests = 0;
private Thread writingThread;
 
//
// read locking
//
 
public synchronized void lockRead() throws InterruptedException {
Thread callingThread = Thread.currentThread();
while(!canGrantReadAccess(callingThread)) {
wait();
}
int accessCount = getReadAccessCount(callingThread);
readingThreads.put(callingThread,accessCount+1);
}
 
public synchronized void unlockRead() {
Thread callingThread = Thread.currentThread();
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1) {
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, accessCount-1);
}
notifyAll();
}
 
private boolean canGrantReadAccess(Thread callingThread) {
if (isWriter(callingThread)) return true;
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
 
private int getReadAccessCount(Thread callingThread) {
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
 
private boolean isReader(Thread callingThread) {
return readingThreads.get(callingThread) != null;
}
 
private boolean hasReaders() {
return readingThreads.size() > 0;
}
 
private boolean isOnlyReader(Thread thread) {
return readingThreads.size() == 1 && readingThreads.get(thread) != null;
}
 
//
// write locking
//
 
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
Thread callingThread = Thread.currentThread();
 
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writers++;
writingThread = callingThread;
}
 
public synchronized void unlockWrite() throws InterruptedException {
writers--;
if(writers == 0){
writingThread = null;
}
notifyAll();
}
 
private boolean canGrantWriteAccess(Thread callingThread) {
if(isOnlyReader(callingThread)) return true;
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}
 
private boolean isWriter(Thread callingThread) {
return writingThread == callingThread;
}
}


Данный класс блокировок можно использовать везде, где требуется синхронизировать читающие и записывающие потоки.

Блокировка чтения/записи. Часть 5

Наконец, последний случай, который мы рассмотрим - если потоку, который имеет доступ на запись, понадобилось вдруг получить также доступ на чтение.

Поток, изменяющий данные, должен всегда получать доступ и на чтение, если это понадобилось. Код из предыдущего поста останется без изменений, кроме метода canGrantReadAccess - туда мы добавим проверку того, что текущий поток является записывающим потоком:

private boolean canGrantReadAccess(Thread callingThread) {
if (isWriter(callingThread)) return true;
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}

Блокировка чтения/записи. Часть 4

Обработаем случай, когда потоку, который имеет доступ на чтение, вдруг понадобилось получить доступ на запись. Он это сможет сделать, если он является единственным потоком, который осуществляет чтение данных.

В предыдущем посте был рассмотрен случай блокировки, который обрабатывает варианты повторной блокировки для чтения и записи. Возьмем код оттуда и немножко модифицируем.

Введем новый метод - isOnlyReader, который проверяет, является ли текущий поток единственным читающим потоком:

private boolean isOnlyReader(Thread thread){
return readingThreads.size() == 1 && readingThreads.get(thread) != null;
}


Также немножко модифицируем метод canGrantWriteAccess:

private boolean canGrantWriteAccess(Thread callingThread){
if(isOnlyReader(callingThread)) return true;
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}


В остальном, код класса ReadWriteLock такой же, как и в
предыдущем посте. Полный код:

public class ReadWriteLock {
private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
private int writers = 0;
private int writeRequests = 0;
private Thread writingThread;
 
//
// read locking
//
 
public synchronized void lockRead() throws InterruptedException {
Thread callingThread = Thread.currentThread();
while(!canGrantReadAccess(callingThread)) {
wait();
}
int accessCount = getReadAccessCount(callingThread);
readingThreads.put(callingThread,accessCount+1);
}
 
public synchronized void unlockRead() {
Thread callingThread = Thread.currentThread();
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1) {
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, accessCount-1);
}
notifyAll();
}
 
private boolean canGrantReadAccess(Thread callingThread) {
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
 
private int getReadAccessCount(Thread callingThread) {
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
 
private boolean isReader(Thread callingThread) {
return readingThreads.get(callingThread) != null;
}
 
private boolean hasReaders() {
return readingThreads.size() > 0;
}
 
private boolean isOnlyReader(Thread thread) {
return readingThreads.size() == 1 && readingThreads.get(thread) != null;
}
 
//
// write locking
//
 
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
Thread callingThread = Thread.currentThread();
 
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writers++;
writingThread = callingThread;
}
 
public synchronized void unlockWrite() throws InterruptedException {
writers--;
if(writers == 0){
writingThread = null;
}
notifyAll();
}
 
private boolean canGrantWriteAccess(Thread callingThread) {
if(isOnlyReader(callingThread)) return true;
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}
 
private boolean isWriter(Thread callingThread) {
return writingThread == callingThread;
}
}

понедельник, 13 сентября 2010 г.

Блокировка чтения/записи. Часть 3

Здесь мы рассмотрим повторное блокирование (reentrant locking) для записывающего потока.

По материалам Read/Write Locks in Java - Write Reentrance.

Одновременно может быть только один поток, который изменяет данные. Никаких других потоков (читающих или пишущих) быть не может. Если мы не предусмотрим повторное блокирование, то может возникнуть следующая ситуация (похожая на ситуацию с повторным блокированием при чтении данных):

  • Поток получает доступ на запись
  • Поток повторно запрашивает доступ на запись
  • Из-за того, что счетчик writers отличен от нуля, поток переводится в состояние ожидания и зависает насовсем

Чтобы этого избежать, нужно при каждом запросе на запись проверять, имеет ли уже данный поток доступ на запись. Если он уже имеет доступ на запись, то мы можем дать ему повторное разрешение.

В коде ниже будем учитывать также повторную блокировку для читающих потоков. Уберем проверку условия на то, можем ли мы дать доступ на запись в отдельный метод - canGrantWriteAccess. В счетчик writers будем записывать число повторных блокировок. Теперь, если writers >0 мы будем проверять, имеет ли этот поток доступ на запись. Если поток имеет доступ на запись, то увеличиваем счетчик writers. Если поток не имеет доступа на запись, то по прежнему переводим его в режим ожидания.

Метод canGrantWriteAccess выглядит так:

private boolean canGrantWriteAccess(Thread callingThread){
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}


Полный код класса таков:

public class ReadWriteLock {
private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
private int writers = 0;
private int writeRequests = 0;
private Thread writingThread;
 
//
// read locking
//
 
public synchronized void lockRead() throws InterruptedException {
Thread callingThread = Thread.currentThread();
while(!canGrantReadAccess(callingThread)){
wait();
}
int accessCount = getReadAccessCount(callingThread);
readingThreads.put(callingThread,accessCount+1);
}
 
public synchronized void unlockRead() {
Thread callingThread = Thread.currentThread();
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1) {
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, accessCount-1);
}
notifyAll();
}
 
private boolean canGrantReadAccess(Thread callingThread) {
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
 
private int getReadAccessCount(Thread callingThread) {
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
 
private boolean isReader(Thread callingThread){
return readingThreads.get(callingThread) != null;
}
 
private boolean hasReaders(){
return readingThreads.size() > 0;
}
 
//
// write locking
//
 
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
Thread callingThread = Thread.currentThread();
 
while(!canGrantWriteAccess(callingThread)){
wait();
}
writeRequests--;
writers++;
writingThread = callingThread;
}
 
public synchronized void unlockWrite() throws InterruptedException{
writers--;
if(writers == 0){
writingThread = null;
}
notifyAll();
}
 
private boolean canGrantWriteAccess(Thread callingThread){
if(hasReaders()) return false;
if(writingThread == null) return true;
if(!isWriter(callingThread)) return false;
return true;
}
 
private boolean isWriter(Thread callingThread){
return writingThread == callingThread;
}
}


Здесь есть учет повторной блокировки для читающих потоков и учет повторной блокировки для записывающего потока. Мы еще не рассмотрели два случая: когда читающий поток хочет получить повторную блокировку на запись, и когда записывающий поток хочет получить повторную блокировку на чтение данных. Эти случаи будут рассмотрены в следующих постах.

Блокировка чтения/записи. Часть 2.

Рассмотрим повторное блокирование (reentrant locking) для читающих потоков.

По материалам Read/Write Locks in Java - Read Reentrance.

Рассмотрим ситуацию:
  • Поток 1 получил доступ на чтение
  • Поток 2 запросил доступ на запись (изменение)
  • Поток 1 повторно запросил получил доступ на чтение, но он окажется заблокированным, т.к. уже имеется запрос на изменение данных от Потока 2

В этом случае мы столкнемся с вариантом дедлока - Поток 2 окажется заблокированным, т.к. уже имеется читающий поток, а Поток 1 будет заблокирован, т.к. есть запрос на изменение данных от Потока 2.

Введем правило для повторного блокирования: поток может получить повторную блокировку на чтение, если он может получить доступ на чтение (нет записывающих потоков, и также нет запросов на изменение) или он уже имеет доступ на чтение (вне зависимости от имеющихся запросов на изменение).

Если поток уже имеет доступ на чтение, это означает, что гарантированно нет потоков, записывающих данные. Но могут быть запросы на изменение данных. Мы их не будем учитывать при отдаче повторной блокировки для уже читающего потока. Ибо учет этих запросов будет приводить в мертвой блокировки читающего потока. А это как раз та ситуация, во избежание которой мы и вводим повторную блокировку.

Чтобы определить, имеет ли поток доступ на чтение, придется держать табличку из потоков, читающих данные. Для каждого потока в этой табличке будем держать счетчик блокировок для этого потока. Когда поток получает доступ на чтение, он добавляется в эту табличку, и счетчик для него устанавливается в 1. Когда поток запрашивает повторный доступ на чтение, счетчик увеличивается на 1. Когда поток отпускает блокировку, то счетчик уменьшается на 1. Если при этом он обнуляется, то поток удаляется из таблички.

Код:


public class ReadWriteLock {
private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
private int writers = 0;
private int writeRequests = 0;
 
public synchronized void lockRead() throws InterruptedException {
Thread callingThread = Thread.currentThread();
while(!canGrantReadAccess(callingThread)){
wait();
}
int accessCount = getReadAccessCount(callingThread);
readingThreads.put(callingThread,accessCount+1);
}
 
public synchronized void unlockRead() {
Thread callingThread = Thread.currentThread();
int accessCount = getReadAccessCount(callingThread);
if(accessCount == 1) {
readingThreads.remove(callingThread);
} else {
readingThreads.put(callingThread, accessCount-1);
}
notifyAll();
}
 
private boolean canGrantReadAccess(Thread callingThread) {
if(writers > 0) return false;
if(isReader(callingThread) return true;
if(writeRequests > 0) return false;
return true;
}
 
private int getReadAccessCount(Thread callingThread) {
Integer accessCount = readingThreads.get(callingThread);
if(accessCount == null) return 0;
return accessCount.intValue();
}
 
private boolean isReader(Thread callingThread){
return readingThreads.get(callingThread) != null;
}
 
public synchronized void lockWrite() throws InterruptedException{
writeRequests++;
 
while(readingThreads.size() > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
}
 
public synchronized void unlockWrite() {
writers--;
notifyAll();
}
}

Постоянные читатели