Thread’ом java не испортишь: часть v
Содержание:
- Методы wait и notify
- Дополнительные материалы
- Пример создания потока. Наследуем класс Thread
- Создание потока с интерфейсом Runnable
- Как выбирать производителя твердотельных накопителей
- Looper
- Остались вопросы? Бесплатная консультация по телефону:
- Нет необходимости писать свое
- Запуск задач с помощью java.util.concurrent.ExecutorService
- Java Thread Join(). Теория
- Новые возможности пакета java.uti.concurrent
- 2 Класс InputStream
- 1 Нововведения в Java 8: Функциональное программирование
- Конкуренция и параллелизм
Методы wait и notify
Последнее обновление: 27.04.2018
Иногда при взаимодействии потоков встает вопрос о извещении одних потоков о действиях других. Например, действия одного потока зависят от результата действий другого потока,
и надо как-то известить один поток, что второй поток произвел некую работу. И для подобных ситуаций у класса Object определено ряд методов:
-
wait(): освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод
-
notify(): продолжает работу потока, у которого ранее был вызван метод
-
notifyAll(): возобновляет работу всех потоков, у которых ранее был вызван метод
Все эти методы вызываются только из синхронизированного контекста — синхронизированного блока или метода.
Рассмотрим, как мы можем использовать эти методы. Возьмем стандартную задачу из прошлой темы — «Производитель-Потребитель» («Producer-Consumer»):
пока производитель не произвел продукт, потребитель не может его купить. Пусть производитель должен произвести 5 товаров, соответственно потребитель
должен их все купить. Но при этом одновременно на складе может находиться не более 3 товаров.
Для решения этой задачи задействуем методы и :
public class Program { public static void main(String[] args) { Store store=new Store(); Producer producer = new Producer(store); Consumer consumer = new Consumer(store); new Thread(producer).start(); new Thread(consumer).start(); } } // Класс Магазин, хранящий произведенные товары class Store{ private int product=0; public synchronized void get() { while (product<1) { try { wait(); } catch (InterruptedException e) { } } product--; System.out.println("Покупатель купил 1 товар"); System.out.println("Товаров на складе: " + product); notify(); } public synchronized void put() { while (product>=3) { try { wait(); } catch (InterruptedException e) { } } product++; System.out.println("Производитель добавил 1 товар"); System.out.println("Товаров на складе: " + product); notify(); } } // класс Производитель class Producer implements Runnable{ Store store; Producer(Store store){ this.store=store; } public void run(){ for (int i = 1; i < 6; i++) { store.put(); } } } // Класс Потребитель class Consumer implements Runnable{ Store store; Consumer(Store store){ this.store=store; } public void run(){ for (int i = 1; i < 6; i++) { store.get(); } } }
Итак, здесь определен класс магазина, потребителя и покупателя. Производитель в методе добавляет в объект Store с помощью его метода
6 товаров. Потребитель в методе в цикле обращается к методу объекта Store для получения
этих товаров. Оба метода Store — и являются синхронизированными.
Для отслеживания наличия товаров в классе Store проверяем значение переменной . По умолчанию товара нет, поэтому переменная равна .
Метод — получение товара должен срабатывать только при наличии хотя бы одного товара. Поэтому в методе
проверяем, отсутствует ли товар:
while (product<1)
Если товар отсутсвует, вызывается метод . Этот метод освобождает монитор объекта Store и блокирует выполнение метода get, пока для этого же монитора не будет вызван
метод .
Когда в методе добавляется товар и вызывается , то метод получает монитор и выходит из
конструкции , так как товар добавлен. Затем имитируется получение покупателем товара. Для этого
выводится сообщение, и уменьшается значение product: . И в конце вызов метода дает сигнал методу продолжить работу.
В методе работает похожая логика, только теперь метод должен срабатывать, если в магазине не более трех товаров. Поэтому в цикле проверяется наличие товара, и если товар уже есть,
то освобождаем монитор с помощью и ждем вызова в методе .
И теперь программа покажет нам другие результаты:
Производитель добавил 1 товар Товаров на складе: 1 Производитель добавил 1 товар Товаров на складе: 2 Производитель добавил 1 товар Товаров на складе: 3 Покупатель купил 1 товар Товаров на складе: 2 Покупатель купил 1 товар Товаров на складе: 1 Покупатель купил 1 товар Товаров на складе: 0 Производитель добавил 1 товар Товаров на складе: 1 Производитель добавил 1 товар Товаров на складе: 2 Покупатель купил 1 товар Товаров на складе: 1 Покупатель купил 1 товар Товаров на складе: 0
Таким образом, с помощью в методе мы ожидаем, когда производитель добавит новый продукт. А после добавления
вызываем , как бы говоря, что на складе освободилось одно место, и можно еще добавлять.
А в методе с помощью мы ожидаем освобождения места на складе. После того, как место освободится, добавляем товар и
через уведомляем покупателя о том, что он может забирать товар.
НазадВперед
Дополнительные материалы
Чтение
- Блог Алексея Шипилёва — знаю, что очевидно, но просто грех не упомянуть
- Блог Черемина Руслана — последнее время не пишет активно, нужно искать в блоге его старые записи, поверьте это стоит того — там кладезь
- Хабр Глеба Смирнова — есть отличные статьи про многопоточность и модель памяти
- Блог Романа Елизарова — заброшен, но археологические раскопки провести нужно. В целом Роман очень много сделал для просветления народа в области теории многопоточного программирования, ищите его в медиа.
Подкасты
- SDCast #62: в гостях Александр Титов и Амир Аюпов, инженеры из Intel и Алексей Маркин, программист из МЦСТ
- SDCast #63: в гостях Алексей Маркин, программист из МЦСТ
- Разбор Полетов: #107 Истории альпинистов
- Разбор Полетов: #154 Кишочки — Атака на Новый Год
Видео
- Computer Science Center — Лекция 11. Модели памяти и проблемы видимости
- Теория и практика многопоточного программирования
Пример создания потока. Наследуем класс Thread
Мы можем наследовать класс для создания собственного класса Thread и переопределить метод . Тогда мы можем создать экземпляр этого класса и вызвать метод для того, чтобы выполнить метод .
Вот простой пример того, как наследоваться от класса Thread:
Java
package ua.com.prologistic;
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
System.out.println(«Стартуем наш поток » + Thread.currentThread().getName());
try {
Thread.sleep(1000);
// для примера будем выполнять обработку базы данных
doDBProcessing();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(«Заканчиваем наш поток » + Thread.currentThread().getName());
}
// метод псевдообработки базы данных
private void doDBProcessing() throws InterruptedException {
Thread.sleep(5000);
}
}
1 |
packageua.com.prologistic; publicclassMyThreadextendsThread{ publicMyThread(Stringname){ super(name); } @Override publicvoidrun(){ System.out.println(«Стартуем наш поток «+Thread.currentThread().getName()); try{ Thread.sleep(1000); // для примера будем выполнять обработку базы данных doDBProcessing(); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println(«Заканчиваем наш поток «+Thread.currentThread().getName()); } // метод псевдообработки базы данных privatevoiddoDBProcessing()throwsInterruptedException{ Thread.sleep(5000); } } |
Вот тестовая программа, показывающая наш поток в работе:
Java
package ua.com.prologistic;
public class ThreadRunExample {
public static void main(String[] args){
Thread t1 = new Thread(new HeavyWorkRunnable(), «t1»);
Thread t2 = new Thread(new HeavyWorkRunnable(), «t2»);
System.out.println(«Стартуем runnable потоки»);
t1.start();
t2.start();
System.out.println(«Runnable потоки в работе»);
Thread t3 = new MyThread(«t3»);
Thread t4 = new MyThread(«t4»);
System.out.println(«Стартуем наши кастомные потоки»);
t3.start();
t4.start();
System.out.println(«Кастомные потоки в работе»);
}
}
1 |
packageua.com.prologistic; publicclassThreadRunExample{ publicstaticvoidmain(Stringargs){ Thread t1=newThread(newHeavyWorkRunnable(),»t1″); Thread t2=newThread(newHeavyWorkRunnable(),»t2″); System.out.println(«Стартуем runnable потоки»); t1.start(); t2.start(); System.out.println(«Runnable потоки в работе»); Thread t3=newMyThread(«t3»); Thread t4=newMyThread(«t4»); System.out.println(«Стартуем наши кастомные потоки»); t3.start(); t4.start(); System.out.println(«Кастомные потоки в работе»); } } |
Создание потока с интерфейсом Runnable
Есть более сложный вариант создания потока. Для создания нового потока нужно реализовать интерфейс Runnable. Вы можете создать поток из любого объекта, реализующего интерфейс Runnable и объявить метод run().
Внутри метода run() вы размещаете код для нового потока. Этот поток завершится, когда метод вернёт управление.
Когда вы объявите новый класс с интерфейсом Runnable, вам нужно использовать конструктор:
В первом параметре указывается экземпляр класса, реализующего интерфейс. Он определяет, где начнётся выполнение потока. Во втором параметре передаётся имя потока.
После создания нового потока, его нужно запустить с помощью метода start(), который, по сути, выполняет вызов метода run().
Создадим новый поток внутри учебного проекта в виде вложенного класса и запустим его.
Внутри конструктора MyRunnable() мы создаём новый объект класса Thread
thread = new Thread(this, "Поток для примера");
В первом параметре использовался объект this, что означает желание вызвать метод run() этого объекта. Далее вызывается метод start(), в результате чего запускается выполнение потока, начиная с метода run(). В свою очередь метод запускает цикл для нашего потока. После вызова метода start(), конструктор MyRunnable() возвращает управление приложению. Когда главный поток продолжает свою работу, он входит в свой цикл. После этого оба потока выполняются параллельно.
Можно запускать несколько потоков, а не только второй поток в дополнение к первому. Это может привести к проблемам, когда два потока пытаюсь работать с одной переменной одновременно.
Как выбирать производителя твердотельных накопителей
Looper
Поток имеет в своём составе сущности Looper, Handler, MessageQueue.
Каждый поток имеет один уникальный Looper и может иметь много Handler.
Считайте Looper вспомогательным объектом потока, который управляет им. Он обрабатывает входящие сообщения, а также даёт указание потоку завершиться в нужный момент.
Поток получает свой Looper и MessageQueue через метод Looper.prepare() после запуска. Looper.prepare() идентифицирует вызывающий потк, создаёт Looper и MessageQueue и связывает поток с ними в хранилище ThreadLocal. Метод Looper.loop() следует вызывать для запуска Looper. Завершить его работу можно через метод looper.quit().
Используйте статический метод getMainLooper() для доступа к Looper главного потока:
Создадим два потока. Один запустим в основном потоке, а второй отдельно от основного. Нам будет достаточно двух кнопок и метки.
Обратите внимание, как запускаются потоки. Первый поток запускается с помощью метода start(), а второй — run()
Затем проверяем, в каком потоке мы находимся.
Эта тема достаточно сложная и для большинства не представляет интереса и необходимости изучать.
В Android потоки в чистом виде используются всё реже и реже, у системы есть собственные способы.
Остались вопросы? Бесплатная консультация по телефону:
Нет необходимости писать свое
Даг Ли создал отличную открытую библиотеку утилит параллельности, , которая включает объекты-мьютексы, семафоры, коллекции, такие как очереди и хэш-таблицы, хорошо работающие при параллельном доступе, и несколько реализаций рабочей очереди. Класс из этого пакета — эффективная, широко использующаяся, правильная реализация пула потоков, основанного на рабочей очереди. Прежде чем пытаться писать собственное программное обеспечение, которое вполне может оказаться неправильным, вы можете рассмотреть использование некоторых утилит в . Ссылки и дополнительную информацию смотрите в разделе .
Библиотека также служит вдохновителем для JSR 166, рабочей группы Java Community Process (JCP), которая будет производить набор параллельных утилит для включения в библиотеку классов Java в пакете , и которая готовит выпуск Java Development Kit 1.5.
Запуск задач с помощью java.util.concurrent.ExecutorService
Облегчив с помощью интерфейса Callable создание задач для параллельного выполнения, пакет java.util.concurrent также берет на себя работу по запуску и остановке потоков. Вместо объекта Thread предлагается использовать объект типа ExecutorService, с помощью которого пользователь может просто поместить задачу в очередь на выполнение и ждать получения результата. Можно сказать, что ExecutorService – это значительно усовершенствованная реализация шаблона WorkerThread.
ExecutorService – это интерфейс, поэтому для выполнения задач используются его конкретные потомки, адаптированные под требования разрабатываемого приложения. Однако программисту нет необходимости создавать собственную реализацию ExecutorService, так как в пакете java.util.concurrent уже присутствуют различные варианты реализации ExecutorService. Доступ к ним можно получить через статические методы служебного класса Executors, метод которого newFixedThreadPool возвращает объект типа ExecutorService со встроенной поддержкой шаблона ThreadPool. Также в классе Executors есть и другие методы для создания объектов ExecutorService с различными свойствами.
Наибольший интерес в ExecutorService представляет метод submit, через который задача ставится в очередь на выполнение. На вход этот метод принимает объект типа Callable или Runnable, а возвращает некий параметризованный объект типа Future. Этот объект можно использовать для доступа к результату выполнения задачи, который будет возвращен из метода call соответствующего Callable-объекта. При этом через объект Future можно проверить, закончено ли уже выполнение задачи – с помощью метода isDone и через метод get получить доступ к результату или исключительной ситуации, если в процессе выполнения задачи произошла ошибка.
Таким образом, при запуске задач с помощью классов из пакета java.util.concurrent не требуется прибегать к низкоуровневой поточной функциональности класса Thread, достаточно создать объект типа ExecutorService с нужными свойствами и передать ему на исполнение задачу типа Callable. Впоследствии можно легко просмотреть результат выполнения этой задачи с помощью объекта Future, как показано в листинге 4.
Листинг 4. Запуск задачи с помощью классов пакета java.util.concurrent
1 public class ExecutorServiceSample { 2 public static void main(String[] args) { 3 //создать ExecutorService на базе пула из пяти потоков 4 ExecutorService es1 = Executors.newFixedThreadPool(5); 5 //поместить задачу в очередь на выполнение 6 Future<String> f1 = es1.submit(new CallableSample()); 7 while(!f1.isDone()) { 8 //подождать пока задача не выполнится 9 } 10 try { 11 //получить результат выполнения задачи 12 System.out.println("task has been completed : " + f1.get()); 13 } catch (InterruptedException ie) { 14 ie.printStackTrace(System.err); 15 } catch (ExecutionException ee) { 16 ee.printStackTrace(System.err); 17 } 18 es1.shutdown(); 19 } 20}
Стоит обратить внимание на строку 18, где происходит остановка объекта ExecutorService с помощью метода shutdown. Дело в том, что потоки в объекте ExecutorService не останавливаются сами, как обычно, поэтому их необходимо явно остановить с помощью этого метода, при этом если в ExecutorService находятся невыполненные задачи, то потоки будут остановлены только, когда завершится последняя задача
Java Thread Join(). Теория
. Этот метод приостановит выполнение текущего потока до тех пор, пока другой поток не закончит свое выполнение. Если поток прерывается, бросается .
: Этот метод приостановит выполнение текущего потока на указанное время в миллисекундах. Выполнение этого метода зависит от реализации ОС, поэтому Java не гарантирует, что текущий поток будет ждать указанное вами время.
: Этот метод приостановит выполнение текущего потока до тех пор, пока другой поток не закончит свое выполнение на время заданное в миллисекундах плюс наносекундах.
Вот простой пример, показывающий использование метода . Цель программы: убедиться в том, что третий поток начнет работу только тогда, когда первый закончит выполнение.
Java
package ua.com.prologistic;
public class ThreadJoinExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), «t1»);
Thread t2 = new Thread(new MyRunnable(), «t2»);
Thread t3 = new Thread(new MyRunnable(), «t3»);
t1.start();
//стартуем второй поток только после 2-секундного ожидания первого потока (или когда он умрет/закончит выполнение)
try {
t1.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
//стартуем 3-й поток только после того, как 1 поток закончит свое выполнение
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t3.start();
//даем всем потокам возможность закончить выполнение перед тем, как программа (главный поток) закончит свое выполнение
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(«Все потоки отработали, завершаем программу»);
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(«Поток начал работу:::» + Thread.currentThread().getName());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(«Поток отработал:::» + Thread.currentThread().getName());
}
}
1 |
packageua.com.prologistic; publicclassThreadJoinExample{ publicstaticvoidmain(Stringargs){ Thread t1=newThread(newMyRunnable(),»t1″); Thread t2=newThread(newMyRunnable(),»t2″); Thread t3=newThread(newMyRunnable(),»t3″); t1.start(); //стартуем второй поток только после 2-секундного ожидания первого потока (или когда он умрет/закончит выполнение) try{ t1.join(2000); }catch(InterruptedExceptione){ e.printStackTrace(); } t2.start(); //стартуем 3-й поток только после того, как 1 поток закончит свое выполнение try{ t1.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } t3.start(); //даем всем потокам возможность закончить выполнение перед тем, как программа (главный поток) закончит свое выполнение try{ t1.join(); t2.join(); t3.join(); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println(«Все потоки отработали, завершаем программу»); } } classMyRunnableimplementsRunnable{ @Override publicvoidrun(){ System.out.println(«Поток начал работу:::»+Thread.currentThread().getName()); try{ Thread.sleep(4000); }catch(InterruptedExceptione){ e.printStackTrace(); } System.out.println(«Поток отработал:::»+Thread.currentThread().getName()); } } |
Результат выполнения программы:
Java
Поток начал работу:::t1
Поток начал работу:::t2
Поток отработал:::t1
Поток начал работу:::t3
Поток отработал:::t2
Поток отработал:::t3
Все потоки отработали, завершаем программу
1 |
Потокначалработу::t1 Потокначалработу::t2 Потокотработал::t1 Потокначалработу::t3 Потокотработал::t2 Потокотработал::t3 Всепотокиотработали,завершаемпрограмму |
Следите за обновлениями раздела Многопоточность и параллелизм в Java
Новые возможности пакета java.uti.concurrent
Платформа Java постоянно развивается, и поэтому к существующей функциональности все время добавляются новые возможности. Иногда новая функциональность берется из уже существующих сторонних библиотек, при этом речь не идет о банальном копировании, а скорее о переосмыслении и доработке уже существующих решений. Подобным способом в версию Java 5 был добавлен пакет java.util.concurrent, включающий в себя множество уже проверенных и хорошо зарекомендовавших себя приемов для параллельного выполнения задач (этот пакет — только одно из множества важных нововведений, представленных в Java 5).
В рамках этой статьи интерес представляют уже готовые к использованию реализации шаблонов WorkerThread и ThreadPool, а также еще один способ реализации задач для параллельного выполнения, кроме упоминавшихся класса Thread и интерфейса Runnable. Ещё в пакете java.util.concurrent находятся два подпакета: java.util.concurrent.locks и java.util.concurrent.atomic, с которыми тоже стоит ознакомиться, так как они значительно упрощают организацию взаимодействия между потоками и параллельного доступа к данным.
2 Класс InputStream
Класс интересен тем, что является классом-родителем для сотен классов-наследников. В нем самом нет никаких данных, однако у него есть методы, которые есть у всех его классов-наследников.
Объекты-потоки вообще редко хранят в себе данные. Поток — это инструмент чтения/записи данных, но не хранения. Хотя бывают и исключения.
Методы класса и всех его классов-наследников:
Методы | Описание |
---|---|
Читает один байт из потока | |
Читает массив байт из потока | |
Читает все байты из потока | |
Пропускает байт в потоке (читает и выкидывает) | |
Проверяет, сколько байт еще осталось в потоке | |
Закрывает поток |
Вкратце пройдемся по этим методам:
Метод
Метод читает один байт из потока и возвращает его. Вас может сбить тип результата — , однако так было сделано, потому что тип — это стандарт всех целых чисел. Три первые байта типа будут равны нулю.
Метод
Это вторая модификация метода . Он позволяет считать из сразу массив байт. Массив для сохранения байт нужно передать в качестве параметра. Метод возвращает число — количество реально прочитанных байт.
Допустим у вас буфер на 10 килобайт, и вы читаете данные из файла с помощью класса . Если файл содержит всего 2 килобайта, все данные будут помещены в массив-буфер, а метод вернет число 2048 (2 килобайта).
Метод
Очень хороший метод. Просто считывает все данные из , пока они не закончатся, и возвращает их виде единого массива байт. Очень удобен для чтения небольших файлов. Большие файлы могут физически не поместиться в память, и метод кинет исключение.
Метод
Этот метод позволяет пропустить n первых байт из объекта . Поскольку данные читаются строго последовательно, этот метод просто вычитывает n первых байт из потока и выбрасывает их.
Возвращает число байт, которые были реально пропущены (если поток закончился раньше, чем прокрутили байт).
Метод
Метод возвращает количество байт, которое еще осталось в потоке
Метод
Метод закрывает поток данных и освобождает связанные с ним внешние ресурсы. После закрытия потока данные из него читать больше нельзя.
Давайте напишем пример программы, которая копирует очень большой файл. Его нельзя весь считать в память с помощью метода . Пример:
Код | Примечание |
---|---|
для чтения из файла для записи в файл Буфер, в который будем считывать данные Пока данные есть в потоке Считываем данные в буфер Записываем данные из буфера во второй поток |
В этом примере мы использовали два класса: — наследник для чтения данных из файла, и класс — наследник для записи данных в файл. О втором классе расскажем немного позднее.
Еще один интересный момент — это переменная . Когда из файла будет читаться последний блок данных, легко может оказаться, что его длина меньше 64Кб. Поэтому в output нужно тоже записать не весь буфер, а только его часть: первые байт. Именно это и делается в методе .
1 Нововведения в Java 8: Функциональное программирование
Вместе с выходом Java 8 в ней появилась мощная поддержка функционального программирования. Можно даже сказать, долгожданная поддержка функционального программирования. Код стал писаться быстрее, хотя читать его стало сложнее
Перед изучением функционального программирования в Java, рекомендуем хорошо разобраться в трех вещах:
- ООП, наследование и интерфейсы (1-2 уровни квеста Java Core).
- Дефолтная реализация методов в интерфейсе.
- Внутренние и анонимные классы.
Хорошая новость заключается в том, что без знания всего этого можно пользоваться многими возможностями функционального программирования в Java. Плохая новость — понять, как именно все устроено и как все работает, без тех же внутренних анонимных классов уже сложно.
В ближайших лекциях мы сосредоточимся на том, как легко и просто пользоваться возможностями функционального программирования в Java, без глубокого понимания, как оно устроено.
Чтобы разобраться во всех нюансах функционального программирования в Java, нужны месяцы. Читать же такой код можно научиться за несколько часов. Поэтому предлагаем начать с малого. Да хоть с тех же потоков ввода-вывода.
Конкуренция и параллелизм
найдешь
- Конкуренция — это способ одновременного решения множества задач
- Параллелизм — это способ выполнения разных частей одной задачи
тут
- Наличие нескольких потоков управления (например Thread в Java, корутина в Kotlin), если поток управления один, то конкурентного выполнения быть не может
- Недетерминированный результат выполнения. Результат зависит от случайных событий, реализации и того как была проведена синхронизация. Даже если каждый поток полностью детерминированный, итоговый результат будет недетерминированным
- Необязательно имеет несколько потоков управления
- Может приводить к детерминированному результату, так например, результат умножения каждого элемента массива на число, не изменится, если умножать его по частям параллельно
- битов (например в 32-разрядных машинах сложение происходит в одно действие, параллельно обрабатывая все 4 байта 32-разрядного числа)
- инструкций (на одном ядре, в одном потоке процессор может выполнять инструкции параллельно, несмотря на то что код последовательный)
- данных (существуют архитектуры с параллельной обработкой данных (Single Instruction Multiple Data), способные выполнять одну инструкцию на большом наборе данных)
- задач (подразумевается наличие нескольких процессоров или ядер)
параллельного