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 – класи з підтримкою атомарних операцій над примітивами та посиланнями. Джерело:
7. Які класи з "канкаренсі" ти знаєш?
Відповідь це питання добре викладено у цій статті . Змилу передруковувати всю її сюди я не бачу, тому наведу описи тільки тих класів, з якими мав честь побіжно ознайомитися. ConcurrentHashMap<K, V> — На відміну відHashtable
блоків і synhronized
на HashMap
, дані представлені у вигляді сегментів, розбитих по hash'ам ключів. В результаті, для доступу до даних лине по сегментах, а не по одному об'єкту. На додаток, ітератори подають дані на певний час і не кидають ConcurrentModificationException
. — Що якщо в класі потрібно синхронізувати доступ до однієї простої змінної типу 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
Цей параметр впливає на використання картки пам'яті та кількість сегментів у картці.
Кількість сегментів буде вибрано як найближчий ступінь двійки, більший за конкурентний рівень. Заниження конкуренціїРівень веде до того, що більш вірогідні блокування потоками сегментів картки під час запису. Завищення показника призводить до неефективного використання пам'яті. Якщо лише один потік змінюватиме карту, а решта читатиме — рекомендується використовувати значення 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
додається код, який дивиться, якщо мютекс зайнятий – то нитка має чекати на його звільнення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ