Лекция 08. Многопоточность и графика

Потоки

Процессы и потоки

Потоки и процессы - это абстракции управления выполнением программы.

  • Процесс - единица исполнения программы, обладает собственным адресным пространством, служебными структурами. Изолирован в системе от остальных процессов.
  • Поток - единица исполнения внутри процесса. Потоки не обладают адресным пространством, но могут исполняться параллельно друг другу.

Каждый процесс имеет хотя бы один выполняющийся поток.

Тот поток, с которого начинается выполнение программы,

называется главным. В языке Java, после создания процесса,

выполнение главного потока начинается с метода main().

Затем, по мере необходимости, в заданных программистом местах,

и при выполнении заданных им же условий, запускаются другие,

побочные потоки.

Модель потока

_images/threads.png

Запуск потоков

Запуск потока

Создадим класс от интефейса Runnable:

class SomeThing implements Runnable  {
    //Этот метод будет выполняться в побочном потоке
    public void run() {
        System.out.println("Привет из побочного потока!");
    }
}
public class Program  {
    static SomeThing sth;
    public static void main(String[] args)
    {
        sth = new SomeThing();
        Thread t = new Thread(sth);
        t.start();
        System.out.println("Главный поток завершён...");
    }
}

Результат:

Главный поток завершён...
Привет из побочного потока!
_images/threads2.png

Другим вариантом является наследование от Thread:

class MyThread extends Thread {
    public void run() {
        System.out.println("Привет из побочного потока!");
    }
}
public class Program  {
    static MyThread t;

    public static void main(String[] args) {
        t = new MyThread(); //Создание потока
        t.start(); //Запуск потока

        System.out.println("Главный поток завершён...");
    }
}

И совсем короткая реализация:

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new HelloThread()).start();
    }

}

Еще один пример:

class Program {
       public static void main(String[] args) {
    Thread t = new Thread (new Runnable() {
       public void run() {
           System.out.println("Привет из побочного потока!");
       }
       }
    }
    t.start();
}

Демоны

Модель потока

В Java процесс завершается тогда, когда завершается последний

его поток. Даже если метод main() уже завершился, но еще выполняются

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

Однако это правило не относится к особому виду потоков – демонам.

Если завершился последний обычный поток процесса, и остались только

потоки-демоны, то они будут принудительно завершены и выполнение

процесса закончится. Чаще всего потоки-демоны используются для

выполнения фоновых задач, обслуживающих процесс в течение его жизни.

Объявить поток демоном достаточно просто — нужно перед запуском

потока вызвать его метод setDaemon(true);

Проверить, является ли поток демоном, можно вызвав его

метод boolean isDaemon();

Демоны

Рассмотрим пример использования потока-демона:

class WorkerThread extends Thread {
    public WorkerThread() {
       setDaemon(true);
    }
    public void run() {
        int count = 0;
        while (true) {
            System.out.println("Hello from Worker "+count++);
            try {
                sleep(5000);
            } catch (InterruptedException e) {
                // handle exception here
            }
        }
    }
}
public class DaemonTest {
    public static void main(String[] args) {
        new WorkerThread().start();
        try {
            Thread.sleep(7500);
        } catch (InterruptedException e) {
            // handle here exception
        }
        System.out.println("Main Thread ending") ;
    }
}

При отсутствии вызова setDaemon() вспомогательный поток

работает как обычный и завершение главного потока не влияет на

его окончание.

Если вспомогательный поток запускается в режиме демона,

он немедленно завершается при завершении главного потока.

Управление работой потока

Управление потоком

Для управления потоком можно использовать три метода:

  • Thread.stop() - прерывает выполнение.
  • Thread.suspend() - приостанавливает выполнение.
  • Thread.resume() - возобновляет выполнение.
  • Thread.sleep() - ожидание (в мс.)
  • Thread.yield() - принудительный возврат управления.
  • Thread.currentThread() - возвращение родительского потока.

Дополнительно:

  • join() - ожидание в потоке завершения другого.
  • start() - начало выполнения.
  • setPriority() - установка приоритета.
  • isAlive() - проверка выполнимости.
  • getId() - возвращение ID-потока

В настоящее время методы stop(),suspend(),resume() объявлены

устаревшими и их использование не поощряется.

Вместо них рекомендуется использовать wait(),notify()

Управление выполнением

Одним из способов управлять потоками является ожидание завершения

потомков со стороны родительского потока.

Если вызывать в главном потоке t.join(), то он будет ожидать завершения работы t:

try {
  sth = new SomeThing();
  Thread t = new Thread(sth);
  t.start();
  t.join();
}
catch(InterruptedException ex) {
    System.out.println("Break!");
}

Синхронизация

Синхронизация используется для согласованной работы нескольких

потоков. Необходимость в ней возникает, например, при попытке

одновременного доступа к некоторому ресурсу.

Если объявить метод как synchronized, то при передаче

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

его до окончания работы.

Состояние гонок - одновременный вызов в потоках одного и того же

метода для одного и того же объекта.

Существуют 2 основных способа синхронизации:

  1. Синхронизация метода
  2. Синхронизация объекта

Взаимодействие потоков

Взаимодействие потоков осуществляется с помощью методов:

  • wait() - вынуждает вызывающий поток уступить монитор и перейти в состояние ожидания, пока другой поток не вызовет метод notify().
  • notify() - возобновляет исполнение потока, из которого был вызван wait().
  • notifyAll() - возобновляет исполнение всех потоков, из которых был вызван метод wait().

Все методы объявлены в классе Object.

Wait и Notify

Рассмотрим пример с использованием wait() и notify().

Разработаем класс Q (очередь) для управления поставоками и

потреблением некоторого ресурса.
class Q {
  int n;  // номер ресурса
  boolean valueSet = false; // ресурс не готов

  synchronized int get() { // получить ресурс
    while(!valueSet)
      try {
        wait(); // ждать
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }
      System.out.println("Получено: " + n);
      valueSet = false;
      notify(); // информировать приостановленный поток
      return n;
  }
  synchronized void put(int n) { // добавление ресурса
    while(valueSet) // пока очередь не пуста, ждем
      try {
        wait();
      } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
      }

      this.n = n;
      valueSet = true;
      System.out.println("Добавлено: " + n);
      notify();
  }
}
class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    try {
      while(true) {
        Thread.sleep(1000);
        q.put(i++); // добавляем ресурс в очередь
      }
    }
    catch(InterruptedException ex) {
      System.out.println("Прервано");
    }
  }
}

Класс ‘’Потребитель’‘

class Consumer implements Runnable {
  Q q;

  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }

  public void run() {
    while(true) {
      q.get();
    }
  }
}

Главный класс программы:

class PCFixed {
  public static void main(String args[]) {
    Q q = new Q();
    new Producer(q);
    new Consumer(q);

    System.out.println("Press Control-C to stop.");
  }
}

Результат выполнения программы:

_images/queue.png

Вопросы для самоконтроля