Параллелизм в сервлетах
Оригинал статьи: ссылка
Обзор
Контейнер Java-сервлетов (или веб-сервер) многопоточен: одновременно может выполняться несколько запросов к одному сервлету. Поэтому при написании сервлета необходимо учитывать параллелизм.Как мы уже говорили ранее, создаётся один и только один экземпляр сервлета, и для каждого нового запроса Servlet Container создаёт новый поток для выполнения doGet() или doPost() методов сервлета.
По умолчанию сервлеты не являются потокобезопасными, программист сам обязан об этом позаботится. В этой главе мы обсудим параллелизм в сервлетах. Это очень важная концепция, поэтому сосредоточьтесь.
Обзор потоков
Поток – это легковесный процесс, который имеет свой собственный стек вызовов и пользуется доступом к открытым данным других потоков в одном и том же процессе (общая куча). Каждый поток имеет свой собственный кэш.
When we say that a program is multithreaded, we mean that same instance of an object spawns multiple threads and process this single instance of code. This means that more than one sequential flow of control runs through the same memory block. So multiple threads execute a single instance of a program and therefore shares instance variables and could possibly be attempting to read and write those shared variable.
Когда мы говорим, что программа многопоточная, мы имеем в виду, что один и тот же экземпляр объекта порождает несколько потоков и обрабатывает единственный элемент кода. Это означает, что через один и тот же блок памяти проходит несколько последовательных потоков управления. Таким образом, несколько потоков выполняют один экземпляр программы и, следовательно, разделяют переменные экземпляра и могут пытаться читать и записывать эти общие переменные.
Давайте рассмотрим простой пример на Java:
public class Counter { int counter=10; public void doSomething() { System.out.println(“Inital Counter = ” + counter); counter ++; System.out.println(“Post Increment Counter = ” + counter); } }
Теперь мы создаём два потока Thread1 и Thread2 для выполнения doSomething(). В результате возможно, что:
- Thread1 считывает значение счетчика, равное 10;
- Отображает Inital Counter = 10 и собирается инкрементировать;
- Перед тем, как Thread1 инкрементирует счётчик, Thread2 также инкрементирует счётчик, изменяя значение счетчика на 11;
- В итоге у Thread1 значение счетчика 10, которое уже устарело.
Этот сценарий возможен в многопоточной среде, такой как сервлеты, потому что переменные экземпляра разделяются всеми потоками, запущенными в одном экземпляре.
Пишем потокобезопасные сервлеты
Надеюсь, в этом разделе вы поймёте проблемы, которые я пытаюсь подчеркнуть. Если у вас есть хоть малейшие сомнения, прочитайте раздел 2 ещё раз.
Есть некоторые моменты, которые мы должны учитывать при написании сервлетов:
-
Service(), doGet(), doPost() или в более общем виде методы doXXX() не должны обновлять или изменять переменные экземпляра, поскольку переменные экземпляра разделяются всеми потоками одного и того же экземпляра.
-
Если есть необходимость модификации переменной экземпляра, то сделайте это в синхронизированном блоке.
-
Оба вышеперечисленных правила применимы и для статических переменных потому, что они также общие.
-
Локальные переменные всегда являются потокобезопасными.
-
Объекты запроса и ответа являются потокобезопасными для использования, поскольку для каждого запроса в ваш сервлет создаётся новый экземпляр и, следовательно, для каждого потока, выполняемого в вашем сервлете.
Ниже приведены два подхода к обеспечению потокобезопасности
- Синхронизируйте блок, в котором вы изменяете экземпляр или статические переменные (см. ниже фрагмент кода).
Мы рекомендуем синхронизировать блок, в котором ваш код изменяет переменные экземпляра вместо синхронизации полного метода ради повышения производительности.
Обратите внимание, что нам нужно сделать блокировку экземпляра сервлета, поскольку мы должны сделать конкретный экземпляр сервлета потокобезопасным.
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ThreadSafeServlet extends HttpServlet { int counter; @override public void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { synchronized (this) { //code in this block is thread-safe so update the instance variable } //other processing; }
- Single Thread Model – внедрите SingleThreadModel-интерфейс, чтобы сделать поток однопоточным, что означает, что только один поток будет выполнять метод service() или doXXX() за раз. Однопоточный сервлет медленнее под нагрузкой, потому что новые запросы должны ждать свободного экземпляра, чтобы быть обработанными.
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ThreadSafeServlet extends HttpServlet implements SingleThreadModel { int counter; // no need to synchronize as implemented SingleThreadModel @override public void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { }
Использование SingleThreadModel устарело, т. к. рекомендуется использовать синхронизированные блоки.
Заключение
Мы должны быть очень осторожны при написании сервлетов, поскольку «по умолчанию сервлеты не являются потокобезопасными».
-
Если ваш сервлет не имеет какой-либо статической или переменной-члена, вам не нужно беспокоиться, и ваш сервлет является потокобезопасным.
-
Если ваш сервлет просто читает переменную экземпляра, ваш сервлет является потокобезопасным.
-
Если вам нужно изменить экземпляр или статические переменные, обновите его в синхронизированном блоке, удерживая блокировку экземпляра.
-
Если вы следуете правилам выше, и в следующий раз кто-то спросит вас: «Является ли сервлет потокобезопасным?» – ответьте уверенно: «По умолчанию они не являются, но МОИ СЕРВЛЕТЫ являются потокобезопасными».
Есть вопрос? Напишите в комментариях!