• ,

Уровень 26. Ответы на вопросы к собеседованию по теме уровня. Часть 2. Вопросы 6-9, 11-12

6. Что такое канкаренси?

Concurrency – это библиотека классов в Java, в которой собрали специальные классы, оптимизированные для работы из нескольких нитей. Эти классы собраны в пакете java.util.concurrent. Их можно схематично поделить по функциональному признаку следующим образом:


Concurrent Collections — набор коллекций, более эффективно работающие в многопоточной среде нежели стандартные универсальные коллекции из java.util пакета. Вместо базового враппера Collections.synchronizedList с блокированием доступа ко всей коллекции используются блокировки по сегментам данных или же оптимизируется работа для параллельного чтения данных по wait-free алгоритмам.

Queues — неблокирующие и блокирующие очереди с поддержкой многопоточности. Неблокирующие очереди заточены на скорость и работу без блокирования потоков. Блокирующие очереди используются, когда нужно «притормозить» потоки «Producer» или «Consumer», если не выполнены какие-либо условия, например, очередь пуста или перепонена, или же нет свободного «Consumer»'a.

Synchronizers — вспомогательные утилиты для синхронизации потоков. Представляют собой мощное оружие в «параллельных» вычислениях.

Executors — содержит в себе отличные фрейморки для создания пулов потоков, планирования работы асинхронных задач с получением результатов.

Locks — представляет собой альтернативные и более гибкие механизмы синхронизации потоков по сравнению с базовыми synchronized, wait, notify, notifyAll.

Atomics — классы с поддержкой атомарных операций над примитивами и ссылками.

Источник:
habrahabr.ru/company/luxoft/blog/157273/

7. Какие классы из «канкаренси» ты знаешь?

Ответ на этот вопрос отлично изложен в этой статье. Смыла перепечатывать всю её сюда я не вижу, поэтому приведу описания только тех классов, с которыми имел честь вскользь ознакомиться.

ConcurrentHashMap<K, V> — В отличие от Hashtable и блоков synhronized на HashMap, данные представлены в виде сегментов, разбитых по hash'ам ключей. В результате, для доступа к данным лочится по сегментам, а не по одному объекту. В дополнение, итераторы представляют данные на определенный срез времени и не кидают ConcurrentModificationException.

AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray — Что если в классе нужно синхронизировать доступ к одной простой переменной типа int? Можно использовать конструкции с synchronized, а при использовании атомарных операций set/get, подойдет также и volatile. Но можно поступить еще лучше, использовав новые классы Atomic*. За счет использования CAS, операции с этими классами работают быстрее, чем если синхронизироваться через synchronized/volatile. Плюс существуют методы для атомарного добавления на заданную величину, а также инкремент/декремент.

8. Как устроен класс ConcurrentHashMap?

К моменту появления ConcurrentHashMap Java-разработчики нуждались в следующей реализации хэш-карты:
  • Потокобезопасность
  • Отсутствие блокировок всей таблицы на время доступа к ней
  • Желательно, чтобы отсутствовали блокировки таблицы при выполнении операции чтения
Основные идеи реализации ConcurrentHashMap следующие:

1. Элементы карты

В отличие от элементов HashMap, Entry в ConcurrentHashMap объявлены как volatile. Это важная особенность, также связанная с изменениями в JMM.

static final class HashEntry<K, V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K, V> next;

    HashEntry(K key, int hash, HashEntry<K, V> next, V value) {
        this .key = key;
        this .hash = hash;
        this .next = next;
        this .value = value;
     }

    @SuppressWarnings("unchecked")
    static final <K, V> HashEntry<K, V>[] newArray(int i) {
        return new HashEntry[i];
    }
}


2. Хэш-функция

В ConcurrentHashMap также используется улучшенная функция хэширования.
Напомню, какой она была в HashMap из JDK 1.2:
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

Версия из ConcurrentHashMap JDK 1.5:
private static int hash(int h) {
    h += (h << 15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h << 3);
    h ^= (h >>> 6);
    h += (h << 2) + (h << 14);
    return h ^ (h >>> 16);
}

В чём необходимость усложнения хэш-функции? Таблицы в хэш-карте имеют длину, определяемую степенью двойки. Для хэш-кодов, двоичные представления которых не различаются в младшей и старшей позиции, мы будем иметь коллизии. Усложнение хэш-функции как раз решает данную проблему, уменьшая вероятность коллизий в карте.

3. Сегменты

Карта делится на N различных сегментов (16 по умолчанию, максимальное значение может быть 16-битным и представлять собой степень двойки). Каждый сегмент представляет собой потокобезопасную таблицу элементов карты. Увеличение количества сегментов будет способствовать тому, что операции модификации будут затрагивать различные сегменты, что уменьшит вероятность блокировок во время выполнения.

4. ConcurrencyLevel

Данный параметр влияет на использование картой памяти и количество сегментов в карте.
Количество сегментов будет выбрано как ближайшая степень двойки, большая чем concurrencyLevel. Занижение concurrencyLevel ведёт к тому, что более вероятны блокировки потоками сегментов карты при записи. Завышение показателя ведёт к неэффективному использованию памяти.

Если лишь один поток будет изменять карту, а остальные будут производить чтение — рекомендуется использовать значение 1.

Итого

Итак, основные преимущества и особенности реализации ConcurrentHashMap:
  • Карта имеет схожий с hashmap интерфейс взаимодействия
  • Операции чтения не требуют блокировок и выполняются параллельно
  • Операции записи зачастую также могут выполняться параллельно без блокировок
  • При создании указывается требуемый concurrencyLevel, определяемый по статистике чтения и записи
  • Элементы карты имеют значение value, объявленное как volatile

Источник: habrahabr.ru/post/132884/

9. Что такое класс Lock?

Для управления доступом к общему ресурсу в качестве альтернативы оператору synchronized мы можем использовать блокировки. Функциональность блокировок заключена в пакете java.util.concurrent.locks.

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

Классы блокировок реализуют интерфейс Lock, который определяет следующие методы:

  • void lock(): ожидает, пока не будет получена блокировка
  • boolean tryLock(): пытается получить блокировку, если блокировка получена, то возвращает true. Если блокировка не получена, то возвращает false. В отличие от метода lock() не ожидает получения блокировки, если она недоступна
  • void unlock(): снимает блокировку
  • Condition newCondition(): возвращает объект Condition, который связан с текущей блокировкой

Организация блокировки в общем случае довольно проста: для получения блокировки вызывается метод lock(), а после окончания работы с общими ресурсами вызывается метод unlock(), который снимает блокировку.

Объект Condition позволяет управлять блокировкой.

Как правило, для работы с блокировками используется класс ReentrantLock из пакета java.util.concurrent.locks. Данный класс реализует интерфейс Lock.

Рассмотрим использование Java Lock API на примере небольшой программы:
И так, пусть у нас есть класс Resource с парочкой потокобезопасных методов и методов, где потокобезопасность не требуется.
public class Resource {
 
    public void doSomething(){
        // пусть здесь происходит работа с базой данных 
    }
     
    public void doLogging(){
        // потокобезопасность для логгирования нам не требуется
    }
}

А теперь берем класс, который реализует интерфейс Runnable и использует методы класса Resource.
public class SynchronizedLockExample implements Runnable{
 
    // экземпляр класса Resource для работы с методами
    private Resource resource;
     
    public SynchronizedLockExample(Resource r){
        this.resource = r;
    }
     
    @Override
    public void run() {
        synchronized (resource) {
            resource.doSomething();
        }
        resource.doLogging();
    }
}

А теперь перепишем приведенную выше программу с использованием Lock API вместо ключевого слова synchronized.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
// класс для работы с Lock API. Переписан с приведенной выше программы,
// но уже без использования ключевого слова synchronized
public class ConcurrencyLockExample implements Runnable{
 
    private Resource resource;
    private Lock lock;
     
    public ConcurrencyLockExample(Resource r){
        this.resource = r;
        this.lock = new ReentrantLock();
    }
     
    @Override
    public void run() {
        try {
            // лочим на 10 секунд
            if(lock.tryLock(10, TimeUnit.SECONDS)){
            resource.doSomething();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            //убираем лок
            lock.unlock();
        }
        // Для логгирования не требуется потокобезопасность
        resource.doLogging();
    }
 
}

Как видно из программы, мы используем метод tryLock(), чтобы убедиться в том, что поток ждет только определенное время. Если он не получает блокировку на объект, то просто логгирует и выходит.

Еще один важный момент. Необходимо использовать блок try-finally, чтобы убедиться в том, что блокировка будет снята, даже если метод doSomething() бросит исключение.

Источники:
metanit.com/java/tutorial/8.9.php
prologistic.com.ua/java-lock-teoriya-i-primer-ispol-zovaniya-concurrency-lock.html

11. Что такое mutex?

Мютекс – это специальный объект для синхронизации нитей/процессов. Он может принимать два состояния – занят и свободен. Если упростить, то мютекс – это boolean-переменная, которая принимает два значения: занят(true) и свободен(false).

Когда нить хочет монопольно владеть некоторым объектом, она помечает его мютекс занятым, а когда закончила работу с ним – помечает его мютекс свободным.

Мютекс прикреплен к каждому объекту в Java. Прямой доступ к мютексу есть только у Java-машины. От программиста он скрыт.

12. Что такое монитор?

Монитор – это специальный механизм (кусок кода) – надстройка над мютексом, который обеспечивает правильную работу с ним. Ведь мало пометить, что объект – занят, надо еще обеспечить, чтобы другие нити не пробовали воспользоваться занятым объектом.

В Java монитор реализован с помощью ключевого слова synchronized.

Когда мы пишем блок synchronized, то компилятор Java заменяет его тремя кусками кода:
  1. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
  2. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
  3. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.


Часть 1.

8 комментариев

Joysi
Понятно написано.
Champion
Большое спасибо за предоставленную информацию!!!
HOS
  • HOS
  • 0
Ответ на 3-й вопрос по ссылке ниже.
spec-zone.ru/RU/Java/Docs/7/api/java/util/Collections.html
lichMax
М-дааа. Хороший перевод. Особенно «полевая сводка» доставляет.
Eli_Ver
В примере некорректный комментарий:
// лочим на 10 секунд
if(lock.tryLock(10, TimeUnit.SECONDS)){
    resource.doSomething();
}


На самом деле, в соответствии с документацией Oracle, такая операция не «лочит на 10 секунд», а ждет максимум 10 секунд, если лок занят:

Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted.
NemchinovSergey
Когда мы пишем блок synchronized, то компилятор Java заменяет его тремя кусками кода:

Я бы это переписал вот так:
1. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.
2. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
3. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
lichMax
Неплохая подборка ответов на вопросы. Только я бы из ответа на первый вопрос перенёс в ответ на второй вопрос схема группировки классов Конкаренси, а также описание этих групп.
pautina
  • pautina
  • 0
  • Комментарий отредактирован 2017-06-24 01:03:50 пользователем pautina
if(lock.tryLock(10, TimeUnit.SECONDS)){
            resource.doSomething();
            }

не одно и то же
synchronized (resource) {
            resource.doSomething();
        }

а так будет равносильно:
lock.lock() {
            resource.doSomething();
        }finally{
    lock.unLock()
}
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.