private Integer nextValue = 0;
public synchronized Integer getNextOrNull() {
if (nextValue < 100000)
return nextValue++;
else
return null;
}
}
В клиентском коде также вносятся изменения:
while (true) {
Integer nextValue = iterator.getNextOrNull();
if (next == null)
break;
// Действия с nextValue
}
В этом случае мы изменяем API своего класса, чтобы он обладал многопоточной поддержкой[80]. Вместо проверки hasNext() клиент должен выполнить проверку null.
В общем случае серверная блокировка предпочтительна по следующим причинам:
• Она сокращает дублирование кода – клиентская блокировка заставляет каждого клиента устанавливать соответствующую блокировку сервера. Если код блокировки размещается на сервере, клиенты могут использовать объект, не беспокоясь о написании дополнительного кода блокировки.
• Она обеспечивает более высокую производительность – в случае однопоточного развертывания потоково-безопасный сервер можно заменить потоково-небезопасным, устраняя все дополнительные затраты.
• Она снижает вероятность ошибок – в случае клиентской блокировки достаточно всего одному программисту забыть установить блокировку, и работа системы будет нарушена.
• Она определяет единую политику использования – политика сосредоточена в одном месте (на сервере), а не во множестве разных мест (то есть у каждого клиента).
Она сокращает область видимости общих переменных — клиент не знает ни о переменных, ни о том, как они блокируются. Все подробности скрыты на стороне сервера. Если что-то сломается, то количество мест, в которых следует искать причину, сокращается.
Что делать, если серверный код вам неподконтролен?
• Используйте паттерн АДАПТЕР, чтобы изменить API и добавить блокировку:
public class ThreadSafeIntegerIterator {
private IntegerIterator iterator = new IntegerIterator();
public synchronized Integer getNextOrNull() {
if(iterator.hasNext())
return iterator.next();
return null;
}
}
• ИЛИ еще лучше – используйте потоково-безопасные коллекции с расширенными интерфейсами.
Повышение производительности
Допустим, вы хотите выйти в сеть и прочитать содержимое группы страниц по списку URL-адресов. По мере чтения страницы обрабатываются для накопления некоторой статистики. После чтения всех страниц выводится сводный отчет.
Следующий класс возвращает содержимое одной страницы по URL-адресу.
public class PageReader {
//...
public String getPageFor(String url) {
HttpMethod method = new GetMethod(url);
try {
httpClient.executeMethod(method);
String response = method.getResponseBodyAsString();
return response;
} catch (Exception e) {
handle(e);
} finally {
method.releaseConnection();
}
}
}
Следующий класс – итератор, предоставляющий содержимое страниц на основании итератора URL-адресов:
public class PageIterator {
private PageReader reader;
private URLIterator urls;
public PageIterator(PageReader reader, URLIterator urls) {
this.urls = urls;
this.reader = reader;
}
public synchronized String getNextPageOrNull() {
if (urls.hasNext())
getPageFor(urls.next());
else
return null;
}
public String getPageFor(String url) {
return reader.getPageFor(url);
}
}
Экземпляр PageIterator может совместно использоваться разными потоками, каждый из которых использует собственный экземпляр PageReader для чтения и обработки страниц, полученных от итератора.
Обратите внимание: блок synchronized очень мал. Он содержит только критическую секцию, расположенную глубоко внутри PageIterator. Старайтесь синхронизировать как можно меньший объем кода.
Вычисление производительности в однопоточной модели
Выполним некоторые простые вычисления. Для наглядности возьмем следующие показатели:
• Время ввода/вывода для получения страницы (в среднем): 1 секунда.
• Время обработки страницы (в среднем): 0,5 секунды.
• Во время операций ввода/вывода процессор загружен на 0%, а во время обработки – на 100%.