Looking for java Keywords? Try Ask4Keywords

Java Language Pitfall - Малые чтения / записи на небуферизованных потоках неэффективны


пример

Рассмотрим следующий код для копирования одного файла в другой:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new FileInputStream(args[0]);
             OutputStream os = new FileOutputStream(args[1])) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

(Мы рассмотрели пропущенную проверку нормальных аргументов, сообщение об ошибках и т. Д., Поскольку они не имеют отношения к точке этого примера.)

Если вы скомпилируете вышеуказанный код и используете его для копирования огромного файла, вы заметите, что он очень медленный. Фактически, это будет, по крайней мере, на пару порядков медленнее, чем стандартные утилиты копирования файлов ОС.

( Добавьте фактические измерения производительности здесь! )

Основная причина того, что приведенный выше пример медленный (в случае большого файла) заключается в том, что он выполняет однобайтные чтения и однобайтные записи в небуферизованных байтовых потоках. Простым способом повышения производительности является обтекание потоков буферизованными потоками. Например:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new BufferedInputStream(
                     new FileInputStream(args[0]));
             OutputStream os = new BufferedOutputStream(
                     new FileOutputStream(args[1]))) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

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

  • С is , данные считываются из файла в буфер за несколько килобайт за раз. Когда вызывается read() , реализация, как правило, возвращает байт из буфера. Он будет считываться только из основного входного потока, если буфер опустел.

  • Поведение для os аналогично. Вызовы os.write(int) записывают одиночные байты в буфер. Данные записываются только в выходной поток при заполнении буфера или при сбросе или закрытии os .

Как насчет потоков, основанных на символах?

Как вам следует знать, Java I / O предоставляет различные API для чтения и записи двоичных и текстовых данных.

  • InputStream и OutputStream являются базовыми API для потокового двоичного ввода-вывода
  • Reader и Writer являются базовыми API-интерфейсами для потокового ввода-вывода.

Для ввода / вывода текста BufferedReader и BufferedWriter являются эквивалентами для BufferedInputStream и BufferedOutputStream .

Почему буферизованные потоки имеют большое значение?

Настоящая причина, по которой буферизованные потоки помогают повысить производительность, связана с тем, как приложение обращается к операционной системе:

  • Java-метод в Java-приложении или вызовы собственных процедур в собственных библиотеках времени выполнения JVM бывают быстрыми. Обычно они выполняют несколько инструкций машины и имеют минимальное влияние на производительность.

  • Напротив, вызовы во время выполнения JVM для операционной системы выполняются не быстро. Они включают нечто вроде «syscall». Типичный шаблон для системного вызова выглядит следующим образом:

    1. Поместите аргументы syscall в регистры.
    2. Выполните команду ловушки SYSENTER.
    3. Обработчик ловушки переключается в привилегированное состояние и изменяет отображение виртуальной памяти. Затем он отправляет код для обработки конкретного системного вызова.
    4. Обработчик syscall проверяет аргументы, опасаясь, что ему не сообщается о доступе к памяти, которую пользовательский процесс не должен видеть.
    5. Выполняется конкретная работа в режиме syscall. В случае read syscall это может включать:
      1. проверяя, что есть данные для чтения в текущей позиции дескриптора файла
      2. вызывая обработчик файловой системы для извлечения требуемых данных с диска (или там, где он хранится) в буферный кеш,
      3. копирование данных из буферного кеша в JVM-адрес
      4. корректировка позиции дескриптора файловой позиции thstream
    6. Вернитесь из syscall. Это влечет за собой повторное изменение сопоставлений виртуальных машин и выключение привилегированного состояния.

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

Учитывая это, причина, по которой буферизованные потоки имеет большое значение, заключается в том, что они резко сокращают количество системных вызовов. Вместо того, чтобы выполнять syscall для каждого вызова read() , буферизованный входной поток считывает большой объем данных в буфер по мере необходимости. Большинство вызовов read() для буферизованного потока выполняют некоторые простые проверки и возвращают byte который был прочитан ранее. Аналогичные рассуждения применяются в случае выходного потока, а также в случаях потока символов.

(Некоторые считают, что производительность буферизованного ввода-вывода происходит из-за несоответствия между размером запроса на чтение и размером блока диска, временной задержкой на диске и такими вещами. Фактически, современная ОС использует ряд стратегий для обеспечения того, чтобы обычно не нужно ждать диска. Это не настоящее объяснение.)

Буферизованные потоки всегда выигрывают?

Не всегда. Буферизованные потоки, безусловно, выигрывают, если ваше приложение собирается делать «маленькие» чтения или записи. Однако, если вашему приложению нужно выполнять большие чтения или записи в / из большого byte[] или char[] , то буферизованные потоки не дадут вам реальных преимуществ. Действительно, может быть даже небольшое (незначительное) исполнение.

Это самый быстрый способ скопировать файл на Java?

Нет, это не так. Когда вы используете API-интерфейсы, основанные на потоке Java, для копирования файла, вы берете на себя стоимость, по крайней мере, одной дополнительной копии данных, хранящейся в памяти. Этого можно избежать, если вы используете NIO ByteBuffer и API Channel . ( Добавьте ссылку на отдельный пример здесь. )