Оптимизация процесса сжатия 20M файлов с 30 секунд до 1 секунды
Существует требование, чтобы 10 фотографий, отправленных с внешнего интерфейса, были обработаны серверной частью, а затем сжаты в сжатый пакет и переданы через сетевой поток. С сжатием файлов в Java раньше никогда не сталкивался, поэтому напрямую нашел пример в интернете и изменил его, и его можно использовать после модификации.Также резко возрастает, и в итоге на сжатие ушло 30 секунд 20M файл. Код для сжатого файла выглядит следующим образом.
1public static void zipFileNoBuffer() {
2 File zipFile = new File(ZIP_FILE);
3 try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) {
4 //开始时间
5 long beginTime = System.currentTimeMillis();
6
7 for (int i = 0; i < 10; i++) {
8 try (InputStream input = new FileInputStream(JPG_FILE)) {
9 zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
10 int temp = 0;
11 while ((temp = input.read()) != -1) {
12 zipOut.write(temp);
13 }
14 }
15 }
16 printInfo(beginTime);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20}
Здесь я нашел изображение 2M и протестировал его десять раз в цикле. Напечатанный результат выглядит следующим образом, время составляет около 30 секунд.
1fileSize:20M
2consum time:29599
Первый процесс оптимизации - с 30 секунд до 2 секунд
Первое, что приходит на ум для оптимизации, это использоватьбуферBufferInputStream
. существуетFileInputStream
серединаread()
Метод считывает только один байт за раз. В исходном коде также есть инструкции.
1/**
2 * Reads a byte of data from this input stream. This method blocks
3 * if no input is yet available.
4 *
5 * @return the next byte of data, or <code>-1</code> if the end of the
6 * file is reached.
7 * @exception IOException if an I/O error occurs.
8 */
9public native int read() throws IOException;
Это вызов собственного метода для взаимодействия с собственной операционной системой для чтения данных с диска. Вызов собственного метода для взаимодействия с операционной системой каждый раз при чтении байта данных занимает очень много времени. Например, теперь у нас есть 30000 байт данных, если мы используемFileInputStream
Затем вам нужно вызвать собственный метод 30 000 раз, чтобы получить данные, и если вы используете буфер (при условии, что начальный размер буфера достаточен для хранения 30 000 байт данных), вам нужно вызвать его только один раз. потому что буфер вызывается в первомread()
Метод будет напрямую считывать данные с диска прямо в память. Затем медленно возвращайте байт за байтом.
BufferedInputStream
Байтовый массив инкапсулирован внутри для хранения данных, размер по умолчанию 8192
Оптимизированный код выглядит следующим образом
1public static void zipFileBuffer() {
2 File zipFile = new File(ZIP_FILE);
3 try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
4 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(zipOut)) {
5 //开始时间
6 long beginTime = System.currentTimeMillis();
7 for (int i = 0; i < 10; i++) {
8 try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(JPG_FILE))) {
9 zipOut.putNextEntry(new ZipEntry(FILE_NAME + i));
10 int temp = 0;
11 while ((temp = bufferedInputStream.read()) != -1) {
12 bufferedOutputStream.write(temp);
13 }
14 }
15 }
16 printInfo(beginTime);
17 } catch (Exception e) {
18 e.printStackTrace();
19 }
20}
вывод
1------Buffer
2fileSize:20M
3consum time:1808
Видно, что по сравнению с первым использованиемFileInputStream
Эффективность значительно улучшилась
Второй проход оптимизации - с 2 секунд до 1 секунды
использовать буферbuffer
Слова уже удовлетворили мои потребности, но с идеей применить то, что я узнал, я подумал об использовании знаний в NIO для их оптимизации.
Использовать канал
зачем использоватьChannel
Шерстяная ткань? Потому что новое в NIOChannel
иByteBuffer
. Именно потому, что их структура больше соответствует тому, как операционная система выполняет ввод-вывод, их скорость значительно выше по сравнению с традиционным вводом-выводом.Channel
как шахта, содержащая уголь, иByteBuffer
Это грузовик, который отправляется в шахту. Другими словами, наше взаимодействие с данными связано сByteBuffer
взаимодействие.
Может генерировать в NIOFileChannel
Есть три класса. соответственноFileInputStream
,FileOutputStream
, и читать и писатьRandomAccessFile
.
Исходный код выглядит следующим образом
1public static void zipFileChannel() {
2 //开始时间
3 long beginTime = System.currentTimeMillis();
4 File zipFile = new File(ZIP_FILE);
5 try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
6 WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) {
7 for (int i = 0; i < 10; i++) {
8 try (FileChannel fileChannel = new FileInputStream(JPG_FILE).getChannel()) {
9 zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));
10 fileChannel.transferTo(0, FILE_SIZE, writableByteChannel);
11 }
12 }
13 printInfo(beginTime);
14 } catch (Exception e) {
15 e.printStackTrace();
16 }
17}
Мы видим, что нет никакой пользы отByteBuffer
для передачи данных, но используяtransferTo
Методы. Этот метод заключается в прямом соединении двух каналов.
1This method is potentially much more efficient than a simple loop
2* that reads from this channel and writes to the target channel. Many
3* operating systems can transfer bytes directly from the filesystem cache
4* to the target channel without actually copying them.
Это текст описания исходного кода, который, вероятно, означает использованиеtransferTo
более эффективен, чем зацикливаниеChannel
прочитайте его, а затем зациклите, чтобы написать другойChannel
хорошо. Операционная система может передавать байты непосредственно из кеша файловой системы в целевойChannel
, не требуя фактическогоcopy
сцена.
Этап копирования — это процесс перехода из пространства ядра в пространство пользователя.
Видно, что скорость несколько улучшилась по сравнению с использованием буферов.
1------Channel
2fileSize:20M
3consum time:1416
Пространство ядра и пространство пользователя
Так почему же процесс из пространства ядра в пространство пользователя идет медленно? Первое, что нам нужно понять, это что такое пространство ядра и пространство пользователя. В обычной операционной системе, чтобы защитить основные ресурсы в системе, система разделена на четыре области, и права доступа увеличиваются по мере того, как вы входите, поэтому Ring0 называется пространством ядра, которое используется для доступа к некоторым ключам. Ресурсы. Ring3 называется пользовательским пространством.
Пользовательский режим, режим ядра: поток в пространстве ядра называется режимом ядра, а поток в пространстве пользователя принадлежит пользовательскому режиму.
Так что, если нам нужен доступ к основным ресурсам в то время, когда приложению (все приложения относятся к пользовательскому режиму) нужен доступ к основным ресурсам? Затем вам нужно вызвать интерфейс, представленный в ядре, для вызова, который называетсясистемный вызов. Например, в это время нашему приложению требуется доступ к файлам на диске. В этот момент приложение вызовет интерфейс системного вызоваopen
Затем ядро обращается к файлу на диске и возвращает содержимое файла приложению. Общий процесс выглядит следующим образом
Прямые и косвенные буферы
Поскольку мы хотим прочитать файл с диска, нам приходится тратить такую большую неудачу. Есть ли простой способ позволить нашему приложению напрямую манипулировать файлами на диске без необходимости передачи ядра? Да, то есть создать прямой буфер.
-
Непрямой буфер: непрямой буфер — это режим ядра, о котором мы упоминали выше в качестве посредника, и ядро каждый раз должно быть посередине в качестве ретранслятора.
-
Прямой буфер: Прямой буфер не требует пространства ядра в качестве данных транзитной копии, но напрямую применяется для пространства в физической памяти.Это пространство отображается в адресное пространство ядра и адресное пространство пользователя, и доступ к данным между приложениями и дисками осуществляется через это напрямую. прикладная физическая память взаимодействует.
Поскольку прямые буферы такие быстрые, почему мы все не используем прямые буферы? Фактически прямой буфер имеет следующие недостатки. Недостатки прямых буферов:
- небезопасный
- Потребляет больше, потому что напрямую не открывает пространство в JVM. Утилизация этой части памяти может зависеть только от механизма сборки мусора, и когда мусор утилизируется, мы не контролируем.
- Когда данные записываются в буфер физической памяти, программа теряет управление этими данными, то есть когда данные окончательно записываются на диск, может определить только операционная система, и прикладная программа уже не может вмешиваться.
Подводя итог, мы используем
transferTo
Метод заключается в непосредственном открытии прямого буфера. Таким образом, производительность значительно улучшилась
Использовать файлы с отображением памяти
Еще одна новая функция в NIO — файлы с отображением памяти.Почему файлы с отображением памяти работают быстро? На самом деле причина та же, что и выше, а именно открытие прямого буфера в памяти. Взаимодействуйте напрямую с данными. Исходный код выглядит следующим образом
1//Version 4 使用Map映射文件
2public static void zipFileMap() {
3 //开始时间
4 long beginTime = System.currentTimeMillis();
5 File zipFile = new File(ZIP_FILE);
6 try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile));
7 WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) {
8 for (int i = 0; i < 10; i++) {
9
10 zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));
11
12 //内存中的映射文件
13 MappedByteBuffer mappedByteBuffer = new RandomAccessFile(JPG_FILE_PATH, "r").getChannel()
14 .map(FileChannel.MapMode.READ_ONLY, 0, FILE_SIZE);
15
16 writableByteChannel.write(mappedByteBuffer);
17 }
18 printInfo(beginTime);
19 } catch (Exception e) {
20 e.printStackTrace();
21 }
22}
печатать следующим образом
1---------Map
2fileSize:20M
3consum time:1305
Вы можете видеть, что скорость аналогична скорости использования Channel.
Использовать трубу
Канал Java NIO — это одностороннее соединение данных между двумя потоками. Труба имеет канал источника и канал стока. Канал источника используется для чтения данных, а канал приемника — для записи данных. Вы можете увидеть введение в исходном коде, что, вероятно, означает, что поток записи будет заблокирован до тех пор, пока поток чтения не прочитает данные из канала. Если данных для чтения нет, поток чтения также будет заблокирован до тех пор, пока поток записи не запишет данные. пока канал не закроется.
1 Whether or not a thread writing bytes to a pipe will block until another
2 thread reads those bytes
Эффект, который я хочу, заключается в следующем. Исходный код выглядит следующим образом
1//Version 5 使用Pip
2public static void zipFilePip() {
3
4 long beginTime = System.currentTimeMillis();
5 try(WritableByteChannel out = Channels.newChannel(new FileOutputStream(ZIP_FILE))) {
6 Pipe pipe = Pipe.open();
7 //异步任务
8 CompletableFuture.runAsync(()->runTask(pipe));
9
10 //获取读通道
11 ReadableByteChannel readableByteChannel = pipe.source();
12 ByteBuffer buffer = ByteBuffer.allocate(((int) FILE_SIZE)*10);
13 while (readableByteChannel.read(buffer)>= 0) {
14 buffer.flip();
15 out.write(buffer);
16 buffer.clear();
17 }
18 }catch (Exception e){
19 e.printStackTrace();
20 }
21 printInfo(beginTime);
22
23}
24
25//异步任务
26public static void runTask(Pipe pipe) {
27
28 try(ZipOutputStream zos = new ZipOutputStream(Channels.newOutputStream(pipe.sink()));
29 WritableByteChannel out = Channels.newChannel(zos)) {
30 System.out.println("Begin");
31 for (int i = 0; i < 10; i++) {
32 zos.putNextEntry(new ZipEntry(i+SUFFIX_FILE));
33
34 FileChannel jpgChannel = new FileInputStream(new File(JPG_FILE_PATH)).getChannel();
35
36 jpgChannel.transferTo(0, FILE_SIZE, out);
37
38 jpgChannel.close();
39 }
40 }catch (Exception e){
41 e.printStackTrace();
42 }
43}
Суммировать
- Жизни нужно учиться везде, и иногда это просто простая оптимизация, позволяющая глубоко изучить множество различных знаний. Поэтому в исследовании мы не должны просить большего понимания, не только знать это знание, но и понимать, зачем мы это делаем.
- Единство знания и действия: изучив знание, попытайтесь применить его снова. Вот как вы можете надежно запомнить его.