Java Gym: Многопоточность в Android и Java, часть 1: основы

Android — это Linux, а это значит, что в нативных библиотеках можно использовать POSIX Thread (pthread). Правда эту тему лучше обсуждать в связке с Android NDK и Java Native Interface(JNI).

Android — это Java, а значит, все что мы знаем о лёгких потоках в Java, верно для Android. Сейчас я приведу основные приёмы многопоточности в Java.

В Java поток представлен классом Thread(полное имя java.lang.Thread), все потоки, в конечном счёте наследуются от Thread. Есть два подхода к созданию своего потока:

  • унаследовать Thread,
  • реализовать Runnable(полное имя java.lang.Runnable).

В первом случае у нас есть более широкие возможности по управлению определенным нами типом потоков, например мы может использовать статические переменные общие для всех однотипных потоков.

class DoSomethingThread extends Thread {

/**
* Здесь можно доопределить, конструктор, статические поля и т.д.
*/
@Override
protected void run() {
super.run();
// здесь можно что-то делать
}
}

Во втором случае мы определяем только основной процесс и инициализируем некоторые данные.

class DoSomething implements Runnable {

@Override
protected void run() {
// здесь можно что-то делать
// нужно отметить, что предка у этого класса нет, а значит
// не нужно вызывать super.run();
}
}

Теперь создадим и запустим наши потоки:

DoSomethingThread thread = new DoSomethingThread();

th.start();
//Ну или вот так:
Thread thread = new Thread(new DoSomething());
thread.start();

Где использовать Runnable, а где Thread?

Двойственность как бы сама вопрошает: А как лучше? А зачем второй метод если есть первый?

Попробую описать когда и что лучше.

Рассмотрим ситуацию, есть большой сервисный поток, который должен работать всё время работы программы и должен быть очень тщательно запущен и завершён. Тут конечно же лучше определить потомка класса Thread, так как можно будет удобно отслеживать весь жизненный цикл потока (можно переопределить start(), можно расширить известные методы.

А вот другая ситуация. Есть пул потоков и они должны выполнять разные задачи, которые зависят от абстрактного клиентского модуля. Здесь вперед выходит второй приём.

У класса Thread есть много полезных методов, и приёмов построенных на их применении. Большинство этих методов статические, то есть можно использовать их не имея ссылки на объект класса Thread, что особенно удобно если мы используем реализацию Runnable вместо собственно Thread.

В принципе все, что выполняется, выполняется в каком-то потоке, единственное, что нам нужно знать, это как мы можем получить на него ссылку. И такой метод есть — это статический метод currentThread(), он вернет нам объект класса Thread, с которым мы можем сделать все что угодно, например получить имя класса (currentThread().getClass().getName()), правда это нам ничего особенного не даст если текущий поток был создан с применением Runnable. Зато этот метод даст нам доступ ко всем нестатическим полям текущего потока при реализации через Runnable.

Передача управления другому потоку

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

class DoTask implements Runnable {

@Override
protected void run() {
while (true) {
/* рабочий код */
yield();
}
}

Такой код гарантирует, что поток будет сам отдавать управление в конце каждого цикла.

Следующий метод — sleep(), метод очень нужный, если известно, что поток нужно вызывать не часто по стандартам компьютерного времени. Скажем раз в секунду или в минуту.

class DoTask implements Runnable {

@Override
protected void run() {
while (true) {
/* рабочий код */
sleep(1000); // спим секунду
}
}

Принимает в аргумент число миллисекунд, которые нужно проспать. Есть вариант двумя аргументами, где первый — миллисекунды, а второй наносекунды. Правда на своей практике я могу вспомнить наносекундную точность, только для анализа частот с АЦП(аналого-цифровой преобразователь) на системе реального времени QNX. Не думаю, что для мобильных приложений такая точность в принципе доступна.

Важно упомянуть о том, как же нормально остановить поток. В потоке есть метод stop(), но он уже очень давно нежелателен для использования(deprecated). Однако мы хотим остановить выполнение потока и тут у нас есть два варианта:

нужно просто остановить и все,

нужно остановить и убедится что он больше не работает.

Чтобы остановить нужно иметь в потоке контрольные точки. Например если это итерационный процесс, можно разместить контрольную точку в условие цикла:

class StopableTaskOne extends Thread {

private volatile boolean mIsStopped = false;
@Override
protected void run() {
super.run();
mIsStopped = false;
while (!mIsStopped) {
/* собственно рабочий код */
}
}
public void stopThis() {
mIsStopped = true; // текущая итерация - последняя
}
}

Жизненный цикл потока

Все хорошо, но может не работать, если поток в одном из non-runnable состояний. Посмотрим на рисунок, с жизненным циклом потока.

Java Gym: Многопоточность в Android и Java, часть 1: основы

Итак, что здесь у нас.

    • Newly Create — между th =‘new Thread();’ и ‘th.start();’;
    • Started Thread (Runnable) — после ‘th.start();’, однако поток не обязан выполняться в этот самый момент;
    • Started Thread (Running) — в этот самый момент выполняется код в этом самом потоке;
    • Started Thread (Dead) — метода run исполнился до конца;
    • Blocked — выполняется другой поток, а наш yield(), sleep(), wait().
      • Теперь посмотрим, какие состояния у нас runnable, а какие non-runnable.

Java Gym: Многопоточность в Android и Java, часть 1: основы

        Фактически спящий, ждущий или читающий что-то поток не остановится описанным методом пока не закончит. Оно наверное не плохо, если читается небольшая xml, но если читает большой видео файл, то лучше бы этот процесс остановить решительней.
class StopableTaskTwo extends Thread {

private volatile boolean mIsStopped = false;
@Override
protected void run() {
super.run();
mIsStopped = false;
while (!mIsStopped) {
try {
/* собственно рабочий код */
sleep(1000);
} catch (InterruptedException ex) {
/* вот теперь все, не смотря на процесс */
}
}
}
public void stopThis() {
mIsStopped = true; // текущая итерация - последняя
this.interrupt(); // и заканчивается она прямо сейчас
}
}

Но предыдущий метод не сработает если вы вместо sleep() поставите к примеру yield(). Для этой ситуации выход такой.

class StopableTaskTwo extends Thread {

private volatile boolean mIsStopped = false;
@Override
protected void run() {
super.run();
mIsStopped = false;
while (!mIsStopped) {
/* собственно рабочий код */
yield();
if (isInterrupted() {
/* вот теперь все */
return ;// ну или break
}
}
}
public void stopThis() {
mIsStopped = true; // текущая итерация - последняя
this.interrupt(); // и заканчивается она прямо сейчас
}
}

Чтобы дождаться завершения потока нужно вызвать метод join() от экземпляра потока и ждать пока этот метод будет пройден. Это может занять время, поэтому лучше не делать это в потоке пользовательского интерфейса. Главное чтобы не возникла рекурсия я жду в потоке, пока закончится поток, который ждёт пока закончится поток… :-)

Продолжение слeдует

Материал предоставлен компанией Softeq Development, FLLC

Java Gym: Многопоточность в Android и Java, часть 1: основы

Рейтинг
( Пока оценок нет )
webnewsite.ru / автор статьи
Загрузка ...

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: