Потоки данных
В Java для описания работы по вводу/выводу используется специальное понятие – поток данных (stream). Поток данных связан с некоторым источником или приемником данных, способных получать или предоставлять информацию. Соответственно, потоки делятся на входные — читающие данные, и на выходные — передающие (записывающие) данные.
Потоки данных — это упорядоченные последовательности данных, которым соответствует определенный источник (source) (для потоков ввода) или получатель (destination) (для потоков вывода). В Java потоки ввода вывода реализуются в пределах иерархии классов, Определенных в пакете jаvа.io. Классы ввода вывода Java исключают необходимость вникать в особенности низкоуровневой организации операционных систем и предоставляют доступ к системным ресурсам посредством методов работы с файлами и иных инструментов.
Все потоки ввода вывода ведут себя одинаково, несмотря на отличия в конкретных физических устройствах, с которыми они связаны. Одни и те же классы и методы ввода вывода применимы к разнотипным устройствам. Абстракция потока ввода может охватывать разные типы ввода: из файла на диске, клавиатуры или сетевого соединения.
2. Типы потоков данных
В JAVA существует 2 типа потоков данных:
- Символьные потоки (text-streams, последовательности 16-битовых символов Unicode), содержащие символы.
- Байтовые потоки (binary-streams), содержащие восьми битную информацию.
Классы разделяются также по направлению потоков:
- Потоки ввода (input)
- Потоки вывода (output)
В Java определены четыре основных абстрактных классов для работы с потоками:
3. Общая схема работы с потоками в Java
Общая схема работы с потоками в Java описывается тремя простыми шагами:
- Создать потоковый объект и ассоциировать его с файлом на диске.
- Пока есть информация, читать/писать очередные данные в/из потока.
- Закрыть поток.
Потоки вообще и в Java
Пото́к выполне́ния (англ. thread) — наименьшая единица обработки, исполнение которой может быть назначено ядром операционной системы.
Поток данных (англ. stream)
в программировании — абстракция, используемая для чтения или записи файлов, сокетов и т. п. в единой манере. (www.wikipedia.org)
Рассмотрим в общих чертах, что такое потоки, для чего и как они работают. Как и чем они используются в Java, и что под этим подразумевается.
Процесс — экземпляр программы , которому выделены системные ресурсы (память, процессор и тд.), выполняется в отдельном адресном пространстве недоступное для других процессов, и соответственно не имеет доступ к структурам другого процесса.
Для каждого процесса ОС создает один главный поток (thread ), который является потоком выполняющихся по очереди команд центрального процессора, который способен создавать другие потоки, пользуясь для этого программным интерфейсом операционной системы.
Потоки для фоновых действий по обслуживанию основных потоков в Java это потоки-демоны
Получается Thread — “подпрограмма”, которая выполняется параллельно с основной программой. С позволяет одновременно выполнять различные программные задачи.
Интересный для нас класс — java.lang.Thread. В этом классе определены все методы, необходимые для создания потоков, управления их состоянием и синхронизации.
Thread позволяет:
- Создавать свой дочерний класс на базе класса Thread. Переопределив метод run, вы получите метод, работающий в рамках отдельного потока.
- Реализовывать в классе интерфейс Runnable и определять метод run, который будет работать как отдельный поток.
Теперь немного о других потоках. Stream — универсальное представление данных, что позволяет различным программам и устройствам получать и передавать информацию.
В отличии от Thread, который вереница или НИТЬ команд, Stream — ПОТОК переносящий информацию от источника к получателю.
Стандартные потоки (Stream) — потоки процесса зарезервированный для выполнения некоторых «стандартных» функций. В Java доступны три стандартных потока, которые всегда открыты .
Стандартный ввод (0) — зарезервирован для чтения команд пользователя или входных данных. При интерактивном запуске программы по умолчанию нацелен на чтение с устройства текстового интерфейса пользователя (клавиатуры).
В Java реализован, как: System.in. Стандартный поток ввода in определен как статический объект класса InputStream, который содержит только простейшие методы для ввода данных.
Стандартный вывод (1) — зарезервирован для вывода данных, как правило (хотя и не обязательно) текстовых. При интерактивном запуске программы по умолчанию нацелен на запись на устройство отображения (монитор).
При фоновом режиме обычно переназначают этот поток в файл. System.out — связан с консолью.
Стандартный поток вывода out создан на базе класса PrintStream, предназначенного для форматированного вывода данных в виде текстовой строки.
Стандартный вывод ошибок (2) — зарезервирован для вывода диагностических и отладочных сообщений в текстовом виде. Чаще всего цель этого потока совпадает со стандартным выводом, с отличием в том, что отладочные сообщения процесса, вывод которого перенаправлен, всё равно попадут пользователю.
System.err — стандартный поток вывода сообщений об ошибках err так же, как и стандартный поток вывода out, создан на базе класса PrintStream.
Базовые классы для работы с файлами и потоками Stream
Вернемся к потокам процесса (Thread).
Все потоки, созданные процессом, выполняются в адресном пространстве этого процесса и имеют доступ к ресурсам процесса.
Однако поток одного процесса не имеет никакого доступа к ресурсам потока другого процесса, так как они работают в разных адресных пространствах.
При необходимости организации взаимодействия между процессами или потоками, принадлежащими разным процессам, следует пользоваться системными средствами, специально предназначенными для этого.
В случае выполнения нескольких потоков внутри процесса, специально предназначенный для этого планировщик ОС, который следит за очередностью выполнения потоков, распределяя время выполнения по прерываниям системного таймера.
Потоки созданные разными процесса конкурируют за процессорное время. Для управления распределением времени для потоков в Java предусмотрены три значения для приоритетов потоков. Это NORM_PRIORITY, MAX_PRIORITY и MIN_PRIORITY.
По умолчанию вновь поток имеет нормальный приоритет NORM_PRIORITY, потоки с таким приоритетом пользуются процессорным времени на равных правах. MAX_PRIORITY выполняются раньше тех что с нормальным приоритетом, MIN_PRIORITY — после выполнения всех “нормальных” .
Процессы и потоки в многопоточной системе, могут обращаться одновременно к одним и тем же ресурсам, что может привести к неправильной работе приложений.
Стеки (анг. stack, область памяти хранящая хранящая локальные переменные и ссылки на объекты) для параллельных потоков — РАЗЛИЧНЫЕ,
а область кучи (анг. heap, “медленное хранилище” объектов) этих же потоков — ОБЩАЯ, что служит источником дополнительных проблем.
Синхронизация- возможность которая есть в Java, позволяющая синхронизировать не только методы, но и целые блоки программы. Объявление их как synchronized разрешает в один момент времени исполнять данный блок только одному потоку.
Помимо “буквальной” синхронизации, можно скоординировать работу потоков “тонкой” блокировкой потоков: блокировка на заданный период времени, временная приостановка и возобновление работы, ожидание извещения (ожидать, продолжить, всем), ожидание завершения потока .
Важный момент: во избежания переполнения памяти или даже падения программы необходимо закрывать потоки явным образом.
Были использованы материалы взятые из:
- www.wikipedia.org
- www.helloworld.ru
- www.progwards.ru
- www.pixbay.com
ShuRuPin, Базовый курс, октябрь 2020
Потоки ввода-вывода. Работа с файлами
Отличительной чертой многих языков программирования является работа с файлами и потоками. В Java основной функционал работы с потоками сосредоточен в классах из пакета java.io .
Ключевым понятием здесь является понятие потока . Хотя понятие «поток» в программировании довольно перегружено и может обозначать множество различных концепций. В данном случае применительно к работе с файлами и вводом-выводом мы будем говорить о потоке (stream), как об абстракции, которая используется для чтения или записи информации (файлов, сокетов, текста консоли и т.д.).
Поток связан с реальным физическим устройством с помощью системы ввода-вывода Java. У нас может быть определен поток, который связан с файлом и через который мы можем вести чтение или запись файла. Это также может быть поток, связанный с сетевым сокетом, с помощью которого можно получить или отправить данные в сети. Все эти задачи: чтение и запись различных файлов, обмен информацией по сети, ввод-ввывод в консоли мы будем решать в Java с помощью потоков.
Объект, из которого можно считать данные, называется потоком ввода , а объект, в который можно записывать данные, — потоком вывода . Например, если надо считать содержание файла, то применяется поток ввода, а если надо записать в файл — то поток вывода.
В основе всех классов, управляющих потоками байтов, находятся два абстрактных класса: InputStream (представляющий потоки ввода) и OutputStream (представляющий потоки вывода)
Но поскольку работать с байтами не очень удобно, то для работы с потоками символов были добавлены абстрактные классы Reader (для чтения потоков символов) и Writer (для записи потоков символов).
Все остальные классы, работающие с потоками, являются наследниками этих абстрактных классов. Основные классы потоков:
Потоки байтов
Класс InputStream
Класс InputStream является базовым для всех классов, управляющих байтовыми потоками ввода. Рассмотрим его основные методы:
- int available() : возвращает количество байтов, доступных для чтения в потоке
- void close() : закрывает поток
- int read() : возвращает целочисленное представление следующего байта в потоке. Когда в потоке не останется доступных для чтения байтов, данный метод возвратит число -1
- int read(byte[] buffer) : считывает байты из потока в массив buffer. После чтения возвращает число считанных байтов. Если ни одного байта не было считано, то возвращается число -1
- int read(byte[] buffer, int offset, int length) : считывает некоторое количество байтов, равное length, из потока в массив buffer. При этом считанные байты помещаются в массиве, начиная со смещения offset, то есть с элемента buffer[offset] . Метод возвращает число успешно прочитанных байтов.
- long skip(long number) : пропускает в потоке при чтении некоторое количество байт, которое равно number
Класс OutputStream
Класс OutputStream является базовым классом для всех классов, которые работают с бинарными потоками записи. Свою функциональность он реализует через следующие методы:
- void close() : закрывает поток
- void flush() : очищает буфер вывода, записывая все его содержимое
- void write(int b) : записывает в выходной поток один байт, который представлен целочисленным параметром b
- void write(byte[] buffer) : записывает в выходной поток массив байтов buffer.
- void write(byte[] buffer, int offset, int length) : записывает в выходной поток некоторое число байтов, равное length , из массива buffer , начиная со смещения offset , то есть с элемента buffer[offset] .
Абстрактные классы Reader и Writer
Абстрактный класс Reader предоставляет функционал для чтения текстовой информации. Рассмотрим его основные методы:
- absract void close() : закрывает поток ввода
- int read() : возвращает целочисленное представление следующего символа в потоке. Если таких символов нет, и достигнут конец файла, то возвращается число -1
- int read(char[] buffer) : считывает в массив buffer из потока символы, количество которых равно длине массива buffer. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1
- int read(CharBuffer buffer) : считывает в объект CharBuffer из потока символы. Возвращает количество успешно считанных символов. При достижении конца файла возвращает -1
- absract int read(char[] buffer, int offset, int count) : считывает в массив buffer, начиная со смещения offset, из потока символы, количество которых равно count
- long skip(long count) : пропускает количество символов, равное count. Возвращает число успешно пропущенных символов
Класс Writer определяет функционал для всех символьных потоков вывода. Его основные методы:
- Writer append(char c) : добавляет в конец выходного потока символ c. Возвращает объект Writer
- Writer append(CharSequence chars) : добавляет в конец выходного потока набор символов chars. Возвращает объект Writer
- abstract void close() : закрывает поток
- abstract void flush() : очищает буферы потока
- void write(int c) : записывает в поток один символ, который имеет целочисленное представление
- void write(char[] buffer) : записывает в поток массив символов
- absract void write(char[] buffer, int off, int len) : записывает в поток только несколько символов из массива buffer. Причем количество символов равно len, а отбор символов из массива начинается с индекса off
- void write(String str) : записывает в поток строку
- void write(String str, int off, int len) : записывает в поток из строки некоторое количество символов, которое равно len, причем отбор символов из строки начинается с индекса off
Функционал, описанный классами Reader и Writer, наследуется непосредственно классами символьных потоков, в частности классами FileReader и FileWriter соответственно, предназначенными для работы с текстовыми файлами.
Теперь рассмотрим конкретные классы потоков.
Введение в многопоточность в Java очень простым языком: Процессы, Потоки и Основы синхронизации
На старте вашей карьеры вы вполне можете обойтись без практических навыков в параллельном программировании, но рано или поздно перед вами встанет задача, требующая от вас таких навыков.
Итак, в данной статье мы поговорим о многопоточности в Java. Тема очень обширная, и я не ставлю целью описать все ее аспекты. Статья рассчитана на людей, только начинающих свое знакомство с многопоточностью. В данной статье мы рассмотрим основу многопоточности Java, такие базовые механизмы синхронизации как ключевые слова volatile и synchronized и очень важную проблематику “Состояние гонки” и “Взаимная блокировка”.
Я выбрал немного необычный подход, связав технические примеры с нашей повседневной жизнью, надеюсь вам понравится. Тема будет раскрыта на примере абстрактной комнаты и людей в находящихся в ней.
Дабы максимально упростить материал, я намеренно буду опускать некоторые нюансы реализации и иерархии многопоточности в Java, усложняющие понимание темы. Если вы рассчитываете на подробный обзор с техническими терминами и формулировками, то данная статья вам не подойдет.
Что такое процессы и потоки
Прежде чем перейти к многопоточности, давайте разберемся, что такое процессы и потоки.
Процесс — это экземпляр выполняющейся программы, простыми словами при запуске любой программы на вашем компьютере, вы порождаете процесс. Он имеет свое собственное адресное пространство памяти и один или несколько потоков.
Поток — это последовательность инструкций, выполняющаяся внутри процесса. Потоки делят адресное пространство памяти процесса, что позволяет им работать параллельно.
Создание и управление потоками
Каждый раз когда вы запускаете вашу программу, т.е. порождаете процесс, JVM (виртуальная машина) создает для вас так называемы главный поток (main thread) в котором ваш код будет исполняться.
Из своего главного потока вы можете создавать множество других потоков которые будут исполняться параллельно вашему главному потоку.
В Java создание и управление потоками осуществляется с использованием класса Thread. Чтобы создать поток, необходимо унаследоваться от класса Thread и переопределить его метод run(), в котором указывается код, который будет выполняться в потоке. Затем создается экземпляр класса Thread и вызывается метод start(), чтобы запустить поток.
class MyThread extends Thread < public void run() < System.out.println("Этот код выполняется в потоке"); >> public class Main < public static void main(String[] args) < MyThread thread = new MyThread(); thread.start(); System.out.println("Этот код выполняется в главном потоке"); >>
Чтобы объяснить более простым языком что такое потоки и как они взаимодействуют, давайте абстрагируемся от кода и представим что ваша программа (процесс) — это комната, а потоки — это люди, находящиеся в этой комнате. В комнате есть различные предметы (объекты), с которыми люди могут взаимодействовать. Когда вы пускаете несколько людей в комнату (создаете новые потоки), они получают доступ к тем же самым предметам.
Однако, когда несколько людей одновременно пытаются взаимодействовать с одним и тем же предметом, могут возникать конфликты, такие как “Состояние гонки” (Race condition) и “Взаимная блокировка” (Deadlocks).
Состояние гонки
Состояние гонки (Race condition) — это ситуация, когда два или более потока одновременно обращаются к общим данным или ресурсам, и результаты их операций зависят от того, в каком порядке выполняются операции. Это может привести к непредсказуемым и нежелательным результатам, таким как неправильные значения или ошибки в программе. В результате состояния гонки данные или ресурсы могут быть повреждены или использованы неправильно.
Давайте рассмотрим “состояние гонки” в рамках нашего абстрактного примера. Представьте что в комнате стоит стол, а на нем полный стакан воды. В этой же комнате находятся два человека, скажем Саша и Петя. И вот Саша решил сделать глоток воды из этого стакана, чуть позже еще глоток, а потом и еще. В реальной комнате это выглядело бы примерно так: Саша каждый раз подходил бы к этому стакану, брал бы его, делал глоток и клал бы на место, а потом и сам возвращался на место. Но компьютер это не комната, в нем есть всякие механизмы оптимизации. К примеру, вместо того, чтобы заставлять Сашу ходить туда-сюда каждый раз когда он захотел сделать глоток воды, он создаст копию стакана для Саши когда тот придет за стаканом в первый раз.
Итак, Саша уже имеет свой стакан (копию того стакана, что остался стоять на столе), и ему уже не надо каждый раз ходить за стаканом, он просто сидит на своем месте и делает три глотка воды. Но, несмотря на то что у Саши копия стакана, а оригинал остался стоять на столе, он знает что должен его отнести на место и заменить своей копией оригинал, это процесс называется синхронизация и необходимо для поддержания актуального состояния уровня воды в стакане (значения переменной).
В то время как Саша пил из своей копии стакана, Петя тоже захотел глотнуть воды для чего подошел к столу где стоит все еще полный стакан воды потому как Петя еще не вернул свой стакан (синхронизация еще не произошла) и соответственно получил свою копию полного стакана, после чего вернулся на место и сделал два глотка. В то время как Петя пил из своей копии стакана, Саша уже закончил пить и отнес свой стакан на место и соответственно заменил им оригинальный стакан (совершил синхронизацию), в итоге количество воды в стакане на столе уменьшилось на 3 глотка. Через какое-то время Петя тоже закончил пить и отнес свой стакан на место и соответственно заменил им стакан, стоящий на столе. В итоге объем воды в стакане на столе — это полный стакан минус два глотка Пети, а глотки Саши утеряны. Это и есть состояние гонки и результаты ее могут быть абсолютно не предсказуемыми.
Для борьбы с этим явлением Java предоставляет различные механизмы, базовым является использование ключевого слова volatile.
Ключевое слово volatile используется для обозначения переменных, которые могут быть изменены несколькими потоками. Оно гарантирует, что изменения переменной видны другим потокам.
Проще говоря, если вернуться к примеру с Сашей и Петей, то каждый раз, когда кто-нибудь из них захочет глотнуть воды, то он будет подходить к стакану брать его и после глотка сразу же ставить на место, да в этом случае уже нет оптимизации, но зато объем воды в стакане всегда актуальный и это избавляет нас от состояния гонки.
Ниже приведен пример кода, при помощи которого вы можете наблюдать результаты “состояние гонки”. При каждом запуске данного кода результаты будут непредсказуемы, хотя ожидалось увидеть цифру 2000. Проблема легко решается добавлением ключевого слова volatile в декларации переменной private volatile int volume = 0;
class Scratch < static class GlassOfWater < private int volume = 0; public int getVolume() < return volume; >public void setVolume(int volume) < this.volume = volume; >> public static void main(String[] args) throws InterruptedException < GlassOfWater glassOfWater = new GlassOfWater(); Thread thread1 = new Thread(() -> < for (int i = 0; i < 1000; i++) < glassOfWater.setVolume(glassOfWater.getVolume() + 1); >>); Thread thread2 = new Thread(() -> < for (int i = 0; i < 1000; i++) < glassOfWater.setVolume(glassOfWater.getVolume() + 1); >>); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Volume is " + glassOfWater.getVolume()); > >
Взаимная блокировка
Взаимная блокировка (Deadlock) — это ситуация, когда два или более потока зацикленно ожидают друг друга, чтобы освободить ресурсы или завершить определенные операции. В результате ни один из потоков не может продолжить свое выполнение, так как каждый из них блокирует ресурсы, необходимые для завершения работы другого потока. Это приводит к тому, что программа останавливается и не может продолжить свое выполнение, пока ситуация deadlock не будет разрешена вручную.
Вернемся в ту же комнату, где нас ожидают Саша и Петя, и представим что в этой же комнате есть два чемоданчика, первый с различными инструментами типа плоскогубцы, молоток, всякие ключи и т.д, а во втором у нас крепеж, ну типа гайки, шурупы, гвозди, саморезы и т.д. Саша решил что-то отремонтировать в комнате и ему понадобились оба эти чемоданчика. И в то же время он хочет быть уверенным что никто ничего от туда не возьмет пока он работает, для этого он запереть на ключи нужные ему чемоданчики. Таким образом каждый раз когда он что-то будет брать или класть в чемоданчик он его отпирает и запирает.
Данный прием в Java называется блокировкой, это как ключ, который только один человек может держать в руках в определенный момент времени. Когда кто-то владеет ключом (блокирует доступ), другие люди должны ждать своей очереди. Блокировка гарантирует, что критическая секция кода будет выполняться только одним потоком за раз. Таким ключиком в языке Java является ключевое слово synchronized.
Ключевое слово synchronized обеспечивает атомарность операций и предотвращает конфликты между потоками. Это слово может быть применено как к методу целиком, так и к отдельному участку кода.
class MyClass < int counter; public synchronized void doSomething() < counter++; >> class MyClass < int counter; public void doSomething() < synchronized(this) < counter++; >> >
С понятием synchronized мы разобрались и можем продолжить обзор того что же такое “Взаимная блокировка”.
Вернемся к Саше, сначала он взял ключ от ящика с инструментами и запер его, но в это же время Петя тоже решил заняться ремонтом и так же решил запереть чемоданчик, но первым он запер чемоданчик с крепежом. Далее Пете нужен чемоданчик с инструментами, он пошел к этому чемоданчику и видит что он уже заперт Сашей, “ну что же раз такое дело то подожду пока Саша завершит работу с ним” подумал Петя. В то же время Саше нужен чемоданчик с крепежом, но он так же видит что чемоданчик уже заперт Петей, в итоге Саша так же решил подождать. Мораль в том что они будут ждать до бесконечности, так и возникает эта самая взаимная блокировка.
Заключение
Мы рассмотрели лишь малую, но очень важную часть многопоточности в Java. Теперь, когда вы ознакомились с базовыми понятиями и это не отпугнуло вас, пришло время углубиться и познакомиться с другими, более продвинутыми механизмами синхронизации. Они включают в себя очереди, флаги и семафоры, которые позволяют координировать доступ и взаимодействие между потоками. Чтобы достичь максимальной производительности в многопоточных приложениях, недостаточно знать и использовать только ключевые слова. Важно также правильно управлять доступом к общим данным и предотвращать конфликты между потоками. Удачи!
- Программирование
- Java
- Параллельное программирование