Рубрики
Без рубрики

Параллелизм в современных языках программирования: Ява

Создание параллельного веб-сервера на Java для сравнения производительности параллелизма с Rust, Go, JS, TS и Kotlin. Помечено как параллелизм, java, jvm, языки.

Первоначально опубликовано по адресу deepu.tech .

Это серия из нескольких частей, в которой я буду говорить о параллелизме в современных языках программирования и буду создавать и сравнивать параллельный веб-сервер, вдохновленный примером из Rust book , на популярных языках, таких как Rust, Go, JavaScript (NodeJS), TypeScript (Deno), Kotlin и Java для сравнения параллелизма и его производительности между этими языками/платформами. Главы этой серии приведены ниже.

  1. Вступление
  2. Параллельный веб-сервер в Rust
  3. Параллельный веб-сервер в Golang
  4. Параллельный веб-сервер на JavaScript с NodeJS
  5. Параллельный веб-сервер в TypeScript с Deno
  6. Параллельный веб-сервер на Java с JVM
  7. Параллельный веб-сервер в Kotlin с JVM
  8. Сравнение и вывод контрольных показателей

Параллелизм в 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”