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

java.util
пакета. Вместо базового враппера Collections.synchronizedList
с блокированием доступа ко всей коллекции используются блокировки по сегментам данных или же оптимизируется работа для параллельного чтения данных по wait-free алгоритмам.
Queues — неблокирующие и блокирующие очереди с поддержкой многопоточности. Неблокирующие очереди заточены на скорость и работу без блокирования потоков. Блокирующие очереди используются, когда нужно «притормозить» потоки «Producer» или «Consumer», если не выполнены какие-либо условия, например, очередь пуста или перепонена, или же нет свободного «Consumer»'a.
Synchronizers — вспомогательные утилиты для синхронизации потоков. Представляют собой мощное оружие в «параллельных» вычислениях.
Executors — содержит в себе отличные фрейморки для создания пулов потоков, планирования работы асинхронных задач с получением результатов.
Locks — представляет собой альтернативные и более гибкие механизмы синхронизации потоков по сравнению с базовыми synchronized
, wait
, notify
, notifyAll
.
Atomics — классы с поддержкой атомарных операций над примитивами и ссылками.
Источник:
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
следующие:
Элементы карты
В отличие от элементов
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]; } }
Хэш-функция
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); }
В чём необходимость усложнения хэш-функции? Таблицы в хэш-карте имеют длину, определяемую степенью двойки. Для хэш-кодов, двоичные представления которых не различаются в младшей и старшей позиции, мы будем иметь коллизии. Усложнение хэш-функции как раз решает данную проблему, уменьшая вероятность коллизий в карте.
Сегменты
Карта делится на N различных сегментов (16 по умолчанию, максимальное значение может быть 16-битным и представлять собой степень двойки). Каждый сегмент представляет собой потокобезопасную таблицу элементов карты. Увеличение количества сегментов будет способствовать тому, что операции модификации будут затрагивать различные сегменты, что уменьшит вероятность блокировок во время выполнения.
ConcurrencyLevel
Данный параметр влияет на использование картой памяти и количество сегментов в карте.
Количество сегментов будет выбрано как ближайшая степень двойки, большая чем concurrencyLevel. Занижение concurrencyLevel ведёт к тому, что более вероятны блокировки потоками сегментов карты при записи. Завышение показателя ведёт к неэффективному использованию памяти. Если лишь один поток будет изменять карту, а остальные будут производить чтение — рекомендуется использовать значение 1.
Итого
Итак, основные преимущества и особенности реализации
ConcurrentHashMap
:- Карта имеет схожий с
hashmap
интерфейс взаимодействия - Операции чтения не требуют блокировок и выполняются параллельно
- Операции записи зачастую также могут выполняться параллельно без блокировок
- При создании указывается требуемый
concurrencyLevel
, определяемый по статистике чтения и записи - Элементы карты имеют значение
value
, объявленное какvolatile
- Карта имеет схожий с
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()
бросит исключение.
Источники:
11. Что такое mutex?
Мютекс – это специальный объект для синхронизации нитей/процессов. Он может принимать два состояния – занят и свободен. Если упростить, то мютекс – это boolean-переменная, которая принимает два значения: занят(true) и свободен(false). Когда нить хочет монопольно владеть некоторым объектом, она помечает его мютекс занятым, а когда закончила работу с ним – помечает его мютекс свободным. Мютекс прикреплен к каждому объекту в Java. Прямой доступ к мютексу есть только у Java-машины. От программиста он скрыт.12. Что такое монитор?
Монитор – это специальный механизм (кусок кода) – надстройка над мютексом, который обеспечивает правильную работу с ним. Ведь мало пометить, что объект – занят, надо еще обеспечить, чтобы другие нити не пробовали воспользоваться занятым объектом. В Java монитор реализован с помощью ключевого словаsynchronized
.
Когда мы пишем блок synchronized, то компилятор Java заменяет его тремя кусками кода:
- В начале блока
synchronized
добавляется код, который отмечает мютекс как занятый. - В конце блока
synchronized
добавляется код, который отмечает мютекс как свободный. - Перед блоком
synchronized
добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Я бы это переписал вот так:
1. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.
2. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
3. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
На самом деле, в соответствии с документацией Oracle, такая операция не «лочит на 10 секунд», а ждет максимум 10 секунд, если лок занят:
spec-zone.ru/RU/Java/Docs/7/api/java/util/Collections.html