Первоначально опубликовано по адресу deepu.tech .
Это серия из нескольких частей, в которой я буду говорить о параллелизме в современных языках программирования и буду создавать и сравнивать параллельный веб-сервер, вдохновленный примером из Rust book , на популярных языках, таких как Rust, Go, JavaScript (NodeJS), TypeScript (Deno), Kotlin и Java для сравнения параллелизма и его производительности между этими языками/платформами. Главы этой серии приведены ниже.
- Вступление
- Параллельный веб-сервер в Rust
- Параллельный веб-сервер в Golang
- Параллельный веб-сервер на JavaScript с NodeJS
- Параллельный веб-сервер в TypeScript с Deno
- Параллельный веб-сервер на Java с JVM
- Параллельный веб-сервер в Kotlin с JVM
- Сравнение и вывод контрольных показателей
Параллелизм в Java
Язык программирования Java и виртуальная машина Java (JVM) были разработаны для поддержки параллельного программирования, и все выполнение происходит в контексте потоков
- Википедия
Java поддерживала параллельное программирование с первых дней своего существования. До Java 1.1 он даже поддерживал зеленые потоки (виртуальные потоки). Предупреждение о спойлере! Он снова возвращается с Project Loom .
Параллельное программирование всегда лежало в основе Java, поскольку оно было нацелено на многопоточные и многоядерные процессоры. Хотя он и не так прост в использовании, как goroutines
, он был мощным и гибким практически для любого варианта использования. Несмотря на свою мощь, он также довольно сложен, особенно когда вам приходится получать доступ к данным между потоками, поскольку механизм по умолчанию в Java, из-за его корней ООП, заключается в использовании параллелизма общего состояния путем синхронизации потоков.
Потоки лежат в основе параллельного и асинхронного программирования на Java. Начиная с JDK 1.1 и далее, эти потоки будут сопоставляться 1:1 с потоками операционной системы. Благодаря своему раннему созданию экосистема также имеет действительно зрелые библиотеки, от HTTP-серверов до параллельных процессоров сообщений и так далее. Асинхронное программирование появилось в Java немного поздно, строительные блоки были там, но практически его можно было использовать только с Java 8, но оно также повзрослело и теперь имеет отличную экосистему с поддержкой реактивного программирования и асинхронного параллелизма.
Java 8 приобрела множество улучшений и упрощений, чтобы упростить параллелизм. Например, стандартные API-интерфейсы Java, такие как Stream API, даже предоставляют способ легко выполнять параллельную обработку , просто вызывая вызов метода в сложных и ресурсоемких конвейерах.
С помощью Java можно выполнять многопоточный параллелизм или параллельное программирование, а также асинхронное программирование. Это означает, что, как мы видели в первой главе , мы можем смешивать и сопоставлять эти модели, чтобы получить максимально возможную производительность для любого варианта использования.
Многопоточность
Java предоставляет строительные блоки для создания потоков операционной системы и управления ими как часть стандартной библиотеки, а также предоставляет реализации, необходимые для параллелизма с общим состоянием с использованием блокировок и синхронизации. Параллелизм передачи сообщений по умолчанию не предусмотрен, но может быть реализован с помощью внешних библиотек, таких как Akka или с использованием/| Actor model реализации. Однако из-за модели памяти разработчик должен убедиться, что в параллельной программе нет скачков данных или утечек памяти.
Чтобы сделать многопоточность еще более эффективной, Java предоставляет способы создания пулов потоков и повторного использования этих потоков для увеличения пропускной способности. Это станет еще лучше, как только Project loom будет выпущен, надеюсь, с Java 17 или 18. Технически Java имеет одну из самых зрелых экосистем, когда дело доходит до многопоточности, и большинство фреймворков Java, которые вы в конечном итоге будете использовать, будут использовать его внутри для повышения производительности.
Асинхронная обработка
Технически асинхронное программирование не является частью параллелизма, но на практике оно идет рука об руку во многих случаях использования и повышает производительность, а также делает использование ресурсов более эффективным. В Java асинхронное программирование достигается с использованием тех же строительных блоков, что и параллельное/параллельное программирование. он же Нити. Это было не очень популярно в Java до Java 8 из-за сложности и, давайте будем честными, отсутствия таких вещей, как лямбды, поддержка функционального программирования, CompletableFuture и так далее.
Последние версии Java предоставляют строительные блоки, необходимые для асинхронного программирования, со стандартными интерфейсами и реализациями. Но имейте в виду, что использование асинхронной модели программирования увеличивает общую сложность, а экосистема все еще развивается. Существует также множество популярных библиотек и фреймворков, таких как Spring и RxJava, которые поддерживают асинхронное/реактивное программирование.
Однако в Java по-прежнему нет никакого синтаксического сахара для async/await но есть альтернативы, такие как EA Async библиотека, которая находится достаточно близко.
Тестирование
Теперь, когда у нас есть некоторое базовое представление о функциях параллелизма в Java, давайте создадим простой параллельный веб-сервер на Java. Поскольку Java предлагает несколько способов достижения этой цели, мы создадим два примера приложений и сравним их. Используемая версия Java является последней (16.0.1) на момент написания статьи.
Многопоточный параллельный веб-сервер
Этот пример ближе к многопоточному примеру Rust, который мы создали в главе rust , я опустил операторы импорта для краткости. Вы можете найти полный пример на GitHub здесь . Мы используем java.net . ServerSocket
для этого. В этом случае мы не используем никаких внешних зависимостей.
public class JavaHTTPServer { public static void main(String[] args) { var count = 0; // count used to introduce delays // bind listener try (var serverSocket = new ServerSocket(8080, 100)) { System.out.println("Server is listening on port 8080"); while (true) { count++; // listen to all incoming requests and spawn each connection in a new thread new ServerThread(serverSocket.accept(), count).start(); } } catch (IOException ex) { System.out.println("Server exception: " + ex.getMessage()); } } } class ServerThread extends Thread { private final Socket socket; private final int count; public ServerThread(Socket socket, int count) { this.socket = socket; this.count = count; } @Override public void run() { var file = new File("hello.html"); try ( // get the input stream var in = new BufferedReader(new InputStreamReader(socket.getInputStream())); // get character output stream to client (for headers) var out = new PrintWriter(socket.getOutputStream()); // get binary output stream to client (for requested data) var dataOut = new BufferedOutputStream(socket.getOutputStream()); var fileIn = new FileInputStream(file) ) { // add 2 second delay to every 10th request if (count % 10 == 0) { System.out.println("Adding delay. Count: " + count); Thread.sleep(2000); } // read the request first to avoid connection reset errors while (true) { String requestLine = in.readLine(); if (requestLine == null || requestLine.length() == 0) { break; } } // read the HTML file var fileLength = (int) file.length(); var fileData = new byte[fileLength]; fileIn.read(fileData); var contentMimeType = "text/html"; // send HTTP Headers out.println("HTTP/1.1 200 OK"); out.println("Content-type: " + contentMimeType); out.println("Content-length: " + fileLength); out.println("Connection: keep-alive"); out.println(); // blank line between headers and content, very important! out.flush(); // flush character output stream buffer dataOut.write(fileData, 0, fileLength); // write the file data to output stream dataOut.flush(); } catch (Exception ex) { System.err.println("Error with exception : " + ex); } } }
Как вы можете видеть, мы привязываем прослушиватель TCP с помощью ServerSocket
на порт 8080 и прослушивает все входящие запросы. Каждый запрос обрабатывается в новом потоке.
Давайте запустим бенчмарк с помощью ApacheBench. Мы сделаем 10000 запросов со 100 одновременными запросами.
❯ ab -c 100 -n 10000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision: 1879490 $> ... Document Path: / Document Length: 176 bytes Concurrency Level: 100 Time taken for tests: 20.326 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 2600000 bytes HTML transferred: 1760000 bytes Requests per second: 491.98 [#/sec] (mean) Time per request: 203.262 [ms] (mean) Time per request: 2.033 [ms] (mean, across all concurrent requests) Transfer rate: 124.92 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 1 1.5 0 13 Processing: 0 201 600.0 1 2023 Waiting: 0 201 600.0 0 2023 Total: 0 202 600.0 1 2025 Percentage of the requests served within a certain time (ms) 50% 1 66% 2 75% 4 80% 6 90% 2000 95% 2001 98% 2003 99% 2006 100% 2025 (longest request)
Как вы можете видеть, поток обработчика запросов переходит в спящий режим на 2 секунды для каждого 10-го запроса. В реальном сценарии сам пул потоков может стать узким местом, и вы, возможно, не сможете установить так много потоков, поскольку ОС может быть не в состоянии предоставить так много, что приведет к увеличению использования ресурсов и узкому месту. В этом простом случае использования, поскольку каждый поток порождает и обрабатывает запрос очень быстро, мы не столкнемся с проблемой.
Итак, давайте посмотрим, можем ли мы найти другое решение без такого узкого места.
Асинхронный параллельный веб-сервер
Этот пример ближе к асинхронному примеру из главы rust , я опустил операторы импорта для краткости. Вы можете найти полный пример на GitHub здесь . Обратите внимание, что мы используем java.nio.channels. AsynchronousServerSocketChannel
здесь и никаких внешних зависимостей.
public class JavaAsyncHTTPServer { public static void main(String[] args) throws Exception { new JavaAsyncHTTPServer().start(); Thread.currentThread().join(); // Wait forever } private void start() throws IOException { // we shouldn't use try with resource here as it will kill the stream var server = AsynchronousServerSocketChannel.open(); server.bind(new InetSocketAddress("127.0.0.1", 8080), 100); // bind listener server.setOption(StandardSocketOptions.SO_REUSEADDR, true); System.out.println("Server is listening on port 8080"); final int[] count = {0}; // count used to introduce delays // listen to all incoming requests server.accept(null, new CompletionHandler<>() { @Override public void completed(final AsynchronousSocketChannel result, final Object attachment) { if (server.isOpen()) { server.accept(null, this); } count[0]++; handleAcceptConnection(result, count[0]); } @Override public void failed(final Throwable exc, final Object attachment) { if (server.isOpen()) { server.accept(null, this); System.out.println("Connection handler error: " + exc); } } }); } private void handleAcceptConnection(final AsynchronousSocketChannel ch, final int count) { var file = new File("hello.html"); try (var fileIn = new FileInputStream(file)) { // add 2 second delay to every 10th request if (count % 10 == 0) { System.out.println("Adding delay. Count: " + count); Thread.sleep(2000); } if (ch != null && ch.isOpen()) { // Read the first 1024 bytes of data from the stream final ByteBuffer buffer = ByteBuffer.allocate(1024); // read the request fully to avoid connection reset errors ch.read(buffer).get(); // read the HTML file var fileLength = (int) file.length(); var fileData = new byte[fileLength]; fileIn.read(fileData); // send HTTP Headers var message = ("HTTP/1.1 200 OK\n" + "Connection: keep-alive\n" + "Content-length: " + fileLength + "\n" + "Content-Type: text/html; charset=utf-8\r\n\r\n" + new String(fileData, StandardCharsets.UTF_8) ).getBytes(); // write the to output stream ch.write(ByteBuffer.wrap(message)).get(); buffer.clear(); ch.close(); } } catch (IOException | InterruptedException | ExecutionException e) { System.out.println("Connection handler error: " + e); } } }
Как вы можете видеть, мы привязываем асинхронный прослушиватель к порту 8080 и прослушиваем все входящие запросы. Каждый запрос обрабатывается в новой задаче, предоставляемой Asynchronousserversocket channel
. Здесь мы не используем никаких пулов потоков, и все входящие запросы обрабатываются асинхронно, и, следовательно, у нас нет узкого места для максимального количества подключений. Но одна вещь, которую вы можете сразу заметить, – это то, что теперь код намного сложнее.
Давайте запустим бенчмарк с помощью ApacheBench. Мы сделаем 10000 запросов со 100 одновременными запросами.
ab -c 100 -n 10000 http://127.0.0.1:8080/ This is ApacheBench, Version 2.3 <$Revision: 1879490 $> ... Document Path: / Document Length: 176 bytes Concurrency Level: 100 Time taken for tests: 20.243 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 2770000 bytes HTML transferred: 1760000 bytes Requests per second: 494.00 [#/sec] (mean) Time per request: 202.431 [ms] (mean) Time per request: 2.024 [ms] (mean, across all concurrent requests) Transfer rate: 133.63 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.6 0 5 Processing: 0 201 600.0 0 2026 Waiting: 0 201 600.0 0 2026 Total: 0 202 600.0 0 2026 Percentage of the requests served within a certain time (ms) 50% 0 66% 1 75% 3 80% 4 90% 2000 95% 2001 98% 2002 99% 2003 100% 2026 (longest request)
Здесь мы получили почти идентичные результаты, этот даже быстрее на 100 мс. Следовательно, эта версия кажется гораздо более эффективной, чем многопоточная версия для данного конкретного варианта использования. Однако ценой дополнительной сложности.
Вывод
Как я объяснил в первой части этой статьи, этот простой сравнительный анализ не является точным представлением для всех случаев использования параллелизма. Это простой тест для очень конкретного случая использования, простого параллельного веб-сервера, который просто обслуживает файл. Идея состоит в том, чтобы увидеть различия в решениях и понять, как работает параллелизм в Java. И для этого конкретного случая использования асинхронные решения действительно кажутся лучшим выбором.
Так следите за обновлениями в следующем посте, где мы рассмотрим параллелизм в Kotlin и создадим тот же вариант использования в Kotlin.
Рекомендации
Если вам понравилась эта статья, пожалуйста, оставьте лайк или комментарий.
Вы можете следить за мной на Twitter и LinkedIn .
Кредит на изображение обложки: Фото Евгения Литовченко на Unsplash
Оригинал: “https://dev.to/deepu105/concurrency-in-modern-programming-languages-java-3l2c”