SingleThreadModel -- достояние истории

19 август, 2003 - 23:00Виктор Вейтман Чем же не угодил специалистам, работающим над новой версией спецификации, Single­Thread­Model -- эта "палочка-выручалочка", облегчающая жизнь неопытным программистам, неуверенно чувствующим себя в многопотоковой среде?


Ловушка для неискушенных

Интерфейс SingleThreadModel по-своему уникален. В нем не объявлен ни один метод, и использующему его разработчику не надо прилагать никаких дополнительных усилий. Однако данный интерфейс не так безобиден, как может показаться на первый взгляд. Чтобы убедиться в этом, поставим небольшой эксперимент.

Создадим сервлет, код которого приведен в Листинге. Он тривиален, более простые программы встречаются разве что в руководствах для начинающих программистов. Его задача -- раз в секунду записывать в файл очередную цифру в интервале от 0 до 9, а по достижении конца интервала вывести клиенту незатейливую надпись "OK!". Важно, однако, что для записи очередной цифры сервлет открывает файл, а после окончания записи сразу же закрывает его. Осталось испытать наше творение, для чего лучше всего воспользоваться Tomcat 4.0, где реализована спецификация Servlet 2.3.

Теперь начинается самое интересное. Расположим два окна Web-броузера так, чтобы они одновременно отображались на экране, в каждом из них обратимся к нашему сервлету и дождемся появления сообщений "OK!". И тут мы с удивлением обнаружим, что суммарное время обработки обоих запросов составило около 12 с (в зависимости от того, насколько быстро вы сможете передать второй запрос после первого). Но ведь каждый сервлет выполняется ровно 10 с, а поскольку в определении класса явно указано implements SingleThreadModel, сервлет попросту не имеет права одновременно обслуживать обращения обеих клиентских программ. Увы, факты -- упрямая вещь! Содержимое файла test.dat, полученного в результате работы сервлета, также подтверждает, что запросы обслуживались никак не поочередно. Вместо ожидаемых двух последовательностей цифр от 0 до 9 мы увидим следующее:

0 1 0 2 1 3 2 4 3 5 4 6 5 7 6 8 7 9 8 9

Разобраться в происходящем поможет фрагмент спецификации: "Если сервлет реализует интерфейс Single­ThreadModel, метод service этого сервлета гарантированно не будет одновременно выполняться в двух потоках. Контейнер сервлетов либо синхронизирует обращения к единственному экземпляру, либо обеспечивает поддержку пула экземпляров и перенаправление запроса свободному сервлету".

Другими словами, контейнер гарантирует лишь отсутствие конфликтов при одновременном обращении к переменным или методам экземпляра сервлета. Однако существуют также и другие ресурсы -- с одним из них мы как раз и встретились в нашем примере. Таким образом, программист, стремящийся облегчить себе жизнь, отказавшись от синхронизации потоков, рискует получить результаты, которые ему будет очень непросто объяснить.

Интерфейс SingleThreadModel может стать ловушкой не только для начинающих. Не исключено, что специалист, имеющий некоторый опыт разработки сервлетов, получив задание реализовать средства удаленного доступа к базе данных, подумает примерно так: "Планируется порядка сотни обращений к базе в сутки. Не буду напрягаться и исключу одновременный доступ, запретив многопотоковое выполнение сервлета". Надо ли говорить, насколько он не прав? Это выяснится, когда два пользователя в одно и то же время решат модифицировать базу. И тогда ущерб, нанесенный организации, будет зависеть лишь от того, прилежно ли системный администратор выполнял резервное копирование.


Жизненный цикл -- не догма

Не стоит думать, что проблемы, связанные с использованием Single­ThreadModel, проявляются лишь при работе с вводом/выводом. Создание сервлета, предназначенного для однопотокового режима, чревато изменением его жизненного цикла. Он, как известно, начинается с выполнения метода init(), затем, рано или поздно, вызывается метод service(), и наконец, перед удалением сервлета происходит обращение к методу destroy().

Допустим, что контейнер поддерживает пул сервлетов, который вначале, естественно, пуст. Первый запрос, полученный контейнером, приводит к инициализации сервлета, т. е. к вызову метода init(). Предположим также, что второй запрос поступил до завершения обработки первого. Для его обслуживания контейнер создаст второй экземпляр сервлета. Что получится в результате? Совершенно верно, init() выполнится во второй раз! Тот факт, что эти методы принадлежат разным экземплярам сервлета и поэтому формально никакого нарушения жизненного цикла не происходит, мало утешит разработчика, если он опрометчиво реализовал в init() действия, не допускающие повторного выполнения.

Многократный вызов метода destroy() наблюдается гораздо реже. Ведь чтобы контейнер принял решение удалить один из нескольких экземпляров сервлета, их должно набраться больше, чем число объектов в пуле, принятое по умолчанию, а это может произойти только при достаточно интенсивных обращениях к сервлету. Но если разработчик предполагает, что запросы будут передаваться часто, он вряд ли использует интерфейс SingleThreadModel. Таким образом, destroy() скорее всего будет выполняться лишь при завершении работы контейнера, но проблема от этого не становится менее острой: многократный вызов init() сам по себе способен доставить немало хлопот.


Нет худа без добра... и наоборот

До сих пор интерфейс Single­Thread­Model был представлен "в черном цвете". Казалось бы, перевод его в разряд средств, не рекомендованных к применению, должен встречаться с ликованием, однако вряд ли это будет именно так. Более того, решение группы, работающей над созданием Servlet 2.4, не понравится многим программистам.

В форумах, посвященных программированию на Java, нередко встречаются жалобы на якобы некорректную работу средств поддержки сеанса. По сообщениям разработчиков, данные, предназначенные для одного клиента, неожиданно появляются у другого -- одним словом, происходит какая-то путаница.

Причина этих и многих других проблем в неправильном использовании переменных. К примеру, если класс сервлета включает большое число методов и все они работают с объектом сеанса, то не слишком искушенный программист буквально на уровне подсознания принимает решение хранить ссылку на данный объект как переменную экземпляра. Однако при этом он не учитывает тот факт, что сервлет работает в многопотоковом режиме. Пусть метод service(), обрабатывая запрос, создал объект HttpSession и присвоил ссылку на него переменной экземпляра. Если одновременно в другом потоке будет выполняться метод с тем же именем, обслуживающий другой запрос, то он попросту заменит значение переменной ссылкой на свой объект сеанса, что не будет ошибкой. В сервлете, работающем в многопотоковом режиме, переменные экземпляра не отличаются от переменных класса, и этот факт часто упускают из виду не очень опытные разработчики.

Что же делать программисту, обнаружившему свою оплошность? Передавать объект сеанса в качестве параметра? Объединить все методы в один (совершенно непригодный для сопровождения) и использовать только локальные переменные? Одни решения лучше, другие хуже, но все они требуют переписывания кода программы. Поэтому проблему использования переменных экземпляра многие решают "одним росчерком пера" -- добавляют к объявлению класса директиву implements Single­Thread­Model. В результате даже при одновременном поступлении нескольких запросов каждый из них будет обрабатываться собственным экземпляром сервлета со своим набором переменных, на который "не посягают" другие потоки.

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

Итак, во вред или во благо обернется новый статус SingleThreadModel? Ответить на этот вопрос очень трудно. Не исключено, что специалистам, готовящим спецификацию Servlet 2.4, не стоило торопиться и вместо отмены SingleThreadModel следовало ввести новый интерфейс, специально предназначенный для создания пула сервлетов. Действительно, не лучше ли было бы сохранить принимаемый по умолчанию интерфейс MultipleThreadModel для работы в многопотоковой среде, но вменить в обязанности SingleThreadModel не только запрет на одновременное выполнение нескольких потоков, но и на создание нескольких экземпляров сервлета, а также ввести новый интерфейс (скажем, PoolModel) для явной организации пула? Однако решение уже, видимо, принято, и рядовым разработчикам остается лишь смириться с ним.

Листинг

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class TestThread extends HttpServlet implements SingleThreadModel

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException

response.setContentType("text/html; charset=windows-1251");
long lastMoment;
for(int i = '0'; i<= '9'; i++)

FileOutputStream testfile = new FileOutputStream("test.dat", true);
testfile.write((byte)i); testfile.write((byte)' ');
testfile.close();
lastMoment = (new GregorianCalendar()).getTimeInMillis();
while ((new GregorianCalendar()).getTimeInMillis() < lastMoment+1000) ;

PrintWriter out = response.getWriter();
out.println("<html><body><h1>OK!</h1></body></html>");