Ядро Java (5) глубокое понимание BIO, NIO, AIO

Java

Руководство: В этой статье вы получите: разницу в производительности между синхронным/асинхронным + блокирующим/неблокирующим; разницу между BIO, NIO и AIO; понимание и реализацию мультиплексирования NIO при работе с сокетами; в то же время освоите дно и основные операции IO Skill.

В чем разница между BIO, NIO и AIO?

В чем разница между одинаковым/асинхронным, блокирующим/неблокирующим?

Каков самый элегантный способ реализации чтения и записи файлов?

Как NIO реализует функцию мультиплексирования?

Ответив на вышеуказанные вопросы, давайте вместе войдем в мир IO.

Прежде чем мы начнем, давайте задумаемся над вопросом: каково полное название «ИО», о котором мы часто говорим?

Возможно, многие люди так же, как и я, запутались, когда увидели этот вопрос Полное название IO на самом деле: аббревиатура Input/Output.

1. Введение в ИО

То, что мы обычно называем BIO, относится к NIO. BIO — это модуль операций ввода-вывода, введенный в начале Java. BIO — это аббревиатура от BlockingIO. Как следует из названия, это означает блокировку ввода-вывода.

1.1 Разница между BIO, NIO и AIO

  1. БИО традиционныйjava.ioПакет реализован на основе потоковой модели.Метод взаимодействия синхронно-блокирующий.То есть при чтении входного потока или выходного потока поток будет заблокирован до завершения действия чтения и записи.Надежное линейное упорядочение при вызывается. Его преимущество в том, что код относительно прост и интуитивно понятен, а недостаток в том, что эффективность и масштабируемость ввода-вывода очень низки, и он легко может стать узким местом в производительности приложения.
  2. NIO — это пакет java.nio, представленный в Java 1.4.Он предоставляет новые абстракции, такие как Channel, Selector, Buffer и т. д. Он может создавать мультиплексированные, синхронные неблокирующие программы ввода-вывода и в то же время обеспечивает высокопроизводительные операции с данными, которые ближе к базовой операционной системе.
  3. AIO — это пакет, представленный после Java 1.7. Это обновленная версия NIO. Он обеспечивает асинхронный неблокирующий режим работы ввода-вывода, поэтому люди называют его AIO (асинхронный ввод-вывод). Асинхронный ввод-вывод реализован на основе событий и механизмов обратного вызова, которые есть операции приложения. После этого он вернется напрямую и не будет там заблокирован. Когда фоновая обработка завершится, операционная система уведомит соответствующий поток для выполнения последующих операций.

1.2 Всестороннее понимание IO

Традиционный IO можно условно разделить на 4 типа:

  • InputStream, OutputStream IO на основе байтовых операций
  • Писатель, читатель на основе персонажей IO
  • Файловый ввод-вывод на основе дисковых операций
  • Ввод-вывод на основе сокетов для сетевых операций

java.netПредставленный ниже Scoket часто классифицируется как синхронный блокирующий ввод-вывод, поскольку сетевое взаимодействие также является поведением ввода-вывода.

java.ioСуществует множество классов и интерфейсов, но обычно они являются подмножеством InputStream, OutputStream, Writer и Reader.Освоение этих четырех классов и File является ключом к эффективному использованию ввода-вывода.

1.3 Использование ввода-вывода

Затем посмотрите на схему наследования и примеры использования InputStream, OutputStream, Writer и Reader.

1.3.1 Использование InputStream

Диаграмма наследования и методы класса, как показано ниже:

Пример использования InputStream:

InputStream inputStream = new FileInputStream("D:\\log.txt");
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String str = new String(bytes, "utf-8");
System.out.println(str);
inputStream.close();

1.3.2 Использование выходного потока

Диаграмма наследования и методы класса, как показано ниже:

Пример использования OutputStream:

OutputStream outputStream = new FileOutputStream("D:\\log.txt",true); // 参数二,表示是否追加,true=追加
outputStream.write("你好,老王".getBytes("utf-8"));
outputStream.close();

1.3.3 Использование модуля записи

Диаграмма наследования Writer и методы класса, как показано ниже:

Пример использования писателя:

Writer writer = new FileWriter("D:\\log.txt",true); // 参数二,是否追加文件,true=追加
writer.append("老王,你好");
writer.close();

1.3.4 Использование считывателя

Диаграмма наследования считывателя и методы класса, как показано ниже:

Пример использования считывателя:

Reader reader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(reader);
StringBuffer bf = new StringBuffer();
String str;
while ((str = bufferedReader.readLine()) != null) {
    bf.append(str + "\n");
}
bufferedReader.close();
reader.close();
System.out.println(bf.toString());

2. Синхронный, асинхронный, блокирующий, неблокирующий

Выше было сказано много понятий о синхронизации, асинхронности, блокировке и неблокировке, далее поговорим о значении четырех из них и анализе производительности, образовавшемся после объединения.

2.1 Синхронный и асинхронный

Синхронизация означает, что когда выполнение задачи должно зависеть от другой задачи, зависимая задача может быть завершена только после ожидания завершения зависимой задачи.Это надежная последовательность задач. Либо все успехи завершаются успехом, либо все неудачи терпят неудачу, а состояние двух задач может оставаться одинаковым. Асинхронность означает, что нет необходимости ждать завершения зависимой задачи, а нужно только уведомить зависимую задачу о том, какую работу нужно завершить, и зависимая задача выполняется немедленно, пока вся задача завершается сама по себе. Что касается того, завершена ли зависимая задача в конце концов, задача, которая зависит от нее, не может быть определена, поэтому это ненадежная последовательность задач. Мы можем использовать телефонные звонки и текстовые сообщения, чтобы хорошо описать синхронные и асинхронные операции.

2.2 Блокировка и неблокировка

Блокировка и неблокировка в основном связаны с потреблением ЦП.Блокировка означает, что ЦП останавливается и ждет, пока медленная операция завершит работу ЦП, прежде чем выполнять другие действия. Неблокирующий означает, что ЦП выполняет другие действия во время выполнения медленной операции, а когда медленная операция завершена, ЦП завершает последующие операции. Хотя неблокирующий метод на поверхности может значительно улучшить использование ЦП, он также приводит к еще одному последствию, заключающемуся в увеличении переключения потоков в системе. Необходимо тщательно оценить, может ли увеличенное время использования ЦП компенсировать стоимость переключения системы.

2.3 То же/другое, резистивное/неблокирующее сочетание

Существует четыре типа комбинаций одинаковых/разных, резистивных/неблокирующих, как показано в следующей таблице:

комбинация анализ производительности
синхронная блокировка Наиболее распространенное использование — это самое простое в использовании, но производительность ввода-вывода обычно низкая, а большая часть ЦП находится в состоянии простоя.
синхронный неблокирующий Распространенным методом повышения производительности ввода-вывода является изменение блокировки ввода-вывода на неблокирующий метод, особенно когда сетевой ввод-вывод представляет собой длинное соединение и одновременно не передается много данных. это очень эффективно для повышения производительности. Этот метод обычно может улучшить производительность ввода-вывода, но увеличит потребление ЦП.Необходимо учитывать, может ли повышенная производительность ввода-вывода компенсировать потребление ЦП, то есть является ли узким местом системы ввод-вывод или ЦП. .
Асинхронная блокировка Этот метод часто используется в распределенных базах данных.Например, при записи записи в распределенную базу данных в Интернете обычно блокируется одна запись, а на другие машины записываются две-три резервные записи. обычно записываются ввод-вывод в асинхронной блокировке. Асинхронная блокировка может повысить эффективность сетевого ввода-вывода, особенно при одновременной записи нескольких копий одних и тех же данных, как описано выше.
Асинхронный неблокирующий Этот метод комбинирования более сложен в использовании и используется только в некоторых очень сложных распределенных ситуациях, например, механизм синхронизации сообщений между кластерами обычно использует этот метод комбинирования ввода-вывода. Например, коммуникационный механизм Gossip Cassandra является асинхронным и неблокирующим. Подходит для одновременной передачи нескольких копий одних и тех же данных на разные машины в кластере.Хотя объем передачи данных небольшой, но очень частый. Такой сетевой ввод/вывод является наиболее производительным.
# 3. Элегантное чтение и запись файлов

До Java 7 файл читался так:

// 添加文件
FileWriter fileWriter = new FileWriter(filePath, true);
fileWriter.write(Content);
fileWriter.close();

// 读取文件
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
StringBuffer bf = new StringBuffer();
String str;
while ((str = bufferedReader.readLine()) != null) {
    bf.append(str + "\n");
}
bufferedReader.close();
fileReader.close();
System.out.println(bf.toString());

В Java 7 представлены файлы (в пакете java.nio), которые значительно упрощают чтение и запись файлов, а именно:

// 写入文件(追加方式:StandardOpenOption.APPEND)
Files.write(Paths.get(filePath), Content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);

// 读取文件
byte[] data = Files.readAllBytes(Paths.get(filePath));
System.out.println(new String(data, StandardCharsets.UTF_8));

Чтение и запись файлов выполняется одной строкой кода, да, это самая элегантная файловая операция.

В разделе «Файлы» есть много полезных методов, таких как создание многослойных папок, которые также просты в написании:

// 创建多(单)层目录(如果不存在创建,存在不会报错)
new File("D://a//b").mkdirs();

4. Мультиплексирование Socket и NIO

Этот раздел поможет вам реализовать самый простой Socket, а также реализовать мультиплексирование NIO, а также реализацию Socket в AIO.

4.1 Традиционная реализация сокетов

Далее мы реализуем простой Socket.Сервер только отправляет информацию клиенту, а затем распечатывает ее клиентом.Код такой:

int port = 4343; //端口号
// Socket 服务器端(简单的发送信息)
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                // 等待连接
                Socket socket = serverSocket.accept();
                Thread sHandlerThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) {
                            printWriter.println("hello world!");
                            printWriter.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                sHandlerThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
sThread.start();

// Socket 客户端(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("客户端:" + s));
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
  • Вызовите метод accept, чтобы заблокировать ожидание подключения клиента;
  • Простой клиент моделируется с помощью Socket, который только подключается, читает и печатает;

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

Вышеупомянутый процесс, как показано ниже:

4.2 Мультиплексирование NIO

Среди вышеупомянутых проблем высокого параллелизма большое значение имеет функция мультиплексирования NIO.

NIO использует однопоточный механизм опроса событий, чтобы определить, что делать, эффективно обнаруживая готовый канал.Блокируется только фаза выбора, что может эффективно избежать проблем, вызванных частым переключением потоков при подключении большого количества клиентских подключений. масштабируемость была значительно улучшена.

// NIO 多路复用
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 4,
        60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();) {
            serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select(); // 阻塞等待就绪的Channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    try (SocketChannel channel = ((ServerSocketChannel) key.channel()).accept()) {
                        channel.write(Charset.defaultCharset().encode("你好,世界"));
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

// Socket 客户端(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));
} catch (IOException e) {
    e.printStackTrace();
}
  • Во-первых, создайте Selector с помощью Selector.open() в качестве роли, подобной диспетчеру;
  • Затем создайте ServerSocketChannel и зарегистрируйтесь в Selector, указав SelectionKey.OP_ACCEPT, сообщите диспетчеру, что он обеспокоен новыми запросами на подключение;
  • Почему мы должны явно настраивать неблокирующий режим? Это связано с тем, что операция регистрации не разрешена в режиме блокировки, и будет выдано исключение IllegalBlockingModeException;
  • Селектор заблокирован в операции выбора и будет разбужен, когда на канале появится запрос на доступ;

На следующем рисунке можно эффективно проиллюстрировать процесс мультиплексирования NIO:

Таким образом, мультиплексирование NIO значительно улучшает способность сервера реагировать на высокий параллелизм.

4.3 Реализация сокета версии AIO

Java 1.7 предоставляет реализацию Socket AIO следующим образом:

// AIO线程复用版
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
        AsynchronousChannelGroup group = null;
        try {
            group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(4));
            AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
            server.accept(null, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
                @Override
                public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
                    server.accept(null, this); // 接收下一个请求
                    try {
                        Future<Integer> f = result.write(Charset.defaultCharset().encode("你好,世界"));
                        f.get();
                        System.out.println("服务端发送时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                        result.close();
                    } catch (InterruptedException | ExecutionException | IOException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
                }
            });
            group.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
});
sThread.start();

// Socket 客户端
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress(InetAddress.getLocalHost(), port));
future.get();
ByteBuffer buffer = ByteBuffer.allocate(100);
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
    @Override
    public void completed(Integer result, Void attachment) {
        System.out.println("客户端打印:" + new String(buffer.array()));
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
Thread.sleep(10 * 1000);

V. Резюме

Вышеупомянутое в основном представляет собой основную операцию ввода-вывода от 1.0 до текущей версии (версия этой статьи) JDK 8. Можно видеть, что ввод-вывод, как более часто используемая базовая функция, претерпел большие изменения в процессе разработки, и это становится все проще и проще в использовании. , операция ввода-вывода также относительно проста для понимания. Один вход - один выход. Если вы освоите ввод и вывод, вы освоите ввод-вывод. Сокет - это интегрированная функция сетевого взаимодействия. Очевидно , мультиплексирование NIO приносит больше преимуществ для Socket.Жизнеспособность и выбор, пользователи могут выбрать соответствующую стратегию кода в соответствии со своими реальными сценариями.

Разумеется, в конце этой статьи я также прикрепляю дядькам пример кода этой статьи:GitHub.com/VIP stone/Срочно…

6. Справочные документы

t.cn/EwUJvWA

Woohoo. IBM.com/developer Я…


Подпишитесь на публичный аккаунт автора в WeChat: