Технологии Java

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

Блокировка (lock, mutex)

  • Только один поток может владеть блокировкой
  • Операции
    • lock — получить блокировку
    • unlock — отдать блокировку

Блокировки в Java

  • Любой объект может служить блокировкой
    • Снятие блокировки производится автоматически
  • Синтаксис
    synchronized (o) { // Получение блокировки
    …
    } // Снятие блокировки
    

Методы экземпляра

  • Метод экземпляра может быть объявлен синхронизованным
    public synchronized int getValue() { … }
    
  • Эквивалентно
    public int getValue() {
        synchronized (this) { … }
    }
    

Методы класса

  • Метод класса может быть объявлен синхронизованным
    class Example {
        public static synchronized int getValue() { … }
    
  • Эквивалентно
    class Example {
        public static int getValue() {
            synchronized (Example.class) { … }
        }
    

Производитель-потребитель

  • Несколько потоков производят данные
  • Несколько потоков потребляют данные
  • Данные могут храниться в очереди (не)ограниченного объема

Интерфейс очереди

  • Хранит один элемент
    class Queue<T> {
        private E data;
        public void set(E data) { … }
        public E get() { … }
    }
    

Производитель

  • Установка значения
    public void set(E data) {
        while (true) { // Активное ожидание
            synchronized (this) {
                if (this.data == null) {
                    this.data = data;
                    break;
                }
            }
        }
    }
    

Потребитель

  • Получение значения
    public E get() {
        while (true) { // Активное ожидание
            synchronized (this) {
                if (data != null) {
                    E d = data;
                    data = null;
                    return d;
                }
            }
        }
    }
    

Монитор

  • Любой объект может быть монитором
  • Передача событий
    • Ожидание условия
    • Извещение одного из ждущих потоков
    • Извещение всех ждущих потоков
  • Нужно владеть блокировкой

Мониторы и блокировки

  • При ожидании монитора блокировка с него снимается
  • При извещении поток не получает управления пока не может получить блокировку обратно
  • Псевдокод
    monitor.unlock()
    monitor.await()
    monitor.lock()
    

Производитель (2)

  • Установка значения
    public synchronized void set(E data)
        throws InterruptedException
    {
        while (this.data != null) {
            wait(); // Пассивное ожидание
        }
        this.data = data;
        notifyAll();
    }
    

Потребитель (2)

  • Получение значения
    public synchronized E get()
        throws InterruptedException {
        while (data == null) {
            wait(); // Пассивное ожидание
        }
        E d = data;
        data = null;
        notifyAll();
        return d;
    }
    

notify() и notifyAll()

  • События одного вида
    • Может обработать любой ждущий поток
    • notify()
  • Несколько видов событий
    • Пробуждение «не того» потока
    • notifyAll()
    • Более дорогая операция

Внезапные пробуждения

  • wait() может завершиться без notify()
    • Проверить наступление события
    • Ожидать всегда в цикле
  • Идиома
    while (требуемое условие) {
        wait();
    }
    

Монитор

  • Разделяемые переменные инкапсулированы в мониторе
  • Код в мониторе исполняется не более чем одним потоком
  • Условия
  • Операции с условиями
    • wait ‒ ожидание условия
    • notify ‒ сообщение об условии одному потоку
    • notifyAll ‒ сообщение об условии всем потокам

Мониторы в Java

  • Любой объект
    • Все методы synchronized
    • Одно событие
  • На блокировках
    • Ручная синхронизация
    • Много событий

Проблемы мониторов

  • Блокировка между операциями отпускается
    • Составные операции не атомарны
    • Конкуренция за блокировку
    • Java: огрубление блокировок
  • Пример
    if (!queue.isEmpty()) {
        // Fail
        Object o = queue.poll();
    }
    

Модель памяти

  • Что может увидеть операция чтения
  • Однопоточная модель
    • Результаты последней записи
  • Многопоточная модель
    • Что такое последняя запись?

Последовательная согласован.

  • Результат эквивалентен какому-то последовательному исполнению
    • Внутри потока — в порядке кода
    • Между потоками — произвольные переключения
  • Не гарантируется Java
    • Иначе нельзя использовать регистры

Атомарность

  • Атомарная операция выполняется как единое целое
    • Значения не появляются «из ниоткуда»
  • Чтение и присвоение значений являются атомарными
    • Кроме long и double

Атомарные чтение и запись

  • Программа
    • Разделяемая переменная
      int a = 0;
      
    • Поток 1
      a = -1;
      
    • Поток 2
      System.out.println(a);
      
  • Возможные результаты
    • 0, −1

Неатомарные чтение и запись

  • Программа
    • Разделяемая переменная
      long a = 0;
      
    • Поток 1
      a = -1;
      
    • Поток 2
      System.out.println(a);
      
  • Возможные результаты
    • 0, −1
    • 0xffffffff, 0xffffffff00000000
    • 2, 3

Пример

Пример

Пример

Пример

Видимость изменений

  • Поток 2 точно изменения, произведенные потоком 1
  • Гарантировано
    • После изменений поток 1 освободил блокировку, которую захватил поток 2
    • После изменения поток 1 создал поток 2
    • Поток 2 дождался окончания потока 1
  • Возможно
    • Любые другие изменения
  • При неправильной синхронизации изменения могут быть видимы в произвольном порядке

Пример

  • Программа
    • Разделяемые переменные
      int a = 0;
      int b = 0;
      
    • Поток 1
      a = 10;
      b = 2;
      
    • Поток 2
      System.out.println(a + b);
      
  • Возможные результаты
    • 0, 10, 12
    • 2

Порядок действий

  • Со своей точки зрения
    • Поток выполняется последовательно
  • С точки зрения других потоков
    • Выполнение может производиться в произвольном порядке

Пример

  • Программа
    • Разделяемая переменная
      int a = 0;
      
    • Поток
      a = 1;
      a = 2;
      
  • Возможные видимые последовательности значений а
    • 0…, 0…01…, 0…02…, 0…01…12…, 1…12…, 2…
    • 2…20…, 2…21…, 0…01…10…

Volatile-переменные

  • Всегда атомарны
  • Чтение из общей памяти
  • Запись в общую память
  • Чтения и записи глобально упорядочены
    • Видимость изменений

Пример 1. Список

  • Программа
    • Разделяемая переменная
      volatile List<String> list = null;
      
    • Поток 1
      List<String> l = new ArrayList<>();
      l.add("Hello");
      list = l;
      
    • Поток 2
      while (list == null) { }
      return list.get(0);
      
  • Не работает без volatile

Пример 2. Синглтон

  • public class Singleton {
        public static volatile Singleton instance;
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
  • Не работает без volatile

Примеры

Содержание

Взаимная блокировка

Содержание

Гарантированный deadlock

public void run() { // 0
    synchronized (o1) { // 1
        o1.notifyAll(); // 2
        synchronized (o2) { // 3
            try {
                o2.wait(); // unlock 4, await 5, lock 6
            } catch (InterruptedException e) {}
        } // 7
    } // 8
}

Диаграмма переходов для deadlock

Барьер

Содержание

Барьер

  • Потоки дожидаются окончания друг друга
    public await(Barrier that) { // 0
        synchronized (this) { // 1
            this.gen++;     // 2
            this.notify();    // 3
        } // 4
        synchronized (that) { // 5
            while (this.gen > that.gen) { // 6
                that.wait(); // unlock 7, await 8, lock 9
            } // 10
        } // 11
    }
    

Диаграмма переходов для барьера

Неизменяемые

  • Не изменяются с момента создания
    • Все поля final
    • Ссылаются только на неизменяемые объекты
  • Потокобезопасны
    • Синхронизация не требуется
  • Immutable

Эффективно неизменяемые

  • Не изменяются с определенного момента
    • Вся инициализация в одном потоке
    • Ссылаются только на (эффективно) неизменяемые объекты
  • Потокобезопасны
    • Корректно опубликованы
  • Effectively immutable

Корректная публикация

  • Корректное получение ссылки на объект
    • Полностью инициализирован
    • Опубликован с барьером
    • Получен с барьером
  • Ссылки до окончания конструктора
    • Переопределяемые методы
    • Регистрация слушателей

Потокобезопасные

  • Внутренняя синхронизация
    • Для одиночных операций синхронизация не требуется
    • Внешняя синхронизация при массовых операциях
  • Thread-safe

Условно потокобезопасные

  • Внешняя синхронизация
    • Синхронизация требуется для всех операций
    • Ответственность на вызывающем
  • Conditionally thread-safe

Потоконебезопасные

  • Не могут использоваться в многопоточной среде
    • Патологический случай
    • Глобальные переменные
    • Надо избегать
  • Thread-hostile

Обеспечение потокобезопасности

Содержание

Делегирование

  • Одно поле
    • Ссылка не изменяется
    • Потокобезопасный объект
    • Атомарные или независимые операции
  • Можно делегировать всю синхронизацию

Привязка к потоку

  • Один поток – один экземпляр
    • Thread-local objects
  • Проблемы с взаимодействием

Thread-local objects

  • Существует по одному на каждый поток
    • Каждый может использовать безопасно
  • Доступ
    • Идентификатор
    • Чтение локального объекта
    • Запись локального объекта

Класс ThreadLocal

Класс InheritableThreadLocal

Заключение

Содержание

Правила синхронизации

  • Работать с разделяемыми значениями только под блокировкой
    • Взял блокировку
    • Прочел/изменил значение
    • Отпустил блокировку
  • Мало блокировок
    • При отсутствии правильной синхронизации потоки могут увидеть практически что угодно
  • Много блокировок
    • Взаимная блокировка

Ожидание

  • Используйте notify[All]() / wait()
    • Всегда под блокировкой
    • Различайте события
  • Не допускайте активного ожидания

Игра Deadlock Empire

Ссылки

Вопросы

???