1. Основи ReadWriteLock
Основна ідея ReadWriteLock полягає в тому, що він надає два локи:
Лок на читання: Дозволяє декільком потокам одночасно читати ресурс, але блокується, якщо будь-який потік має лок на запис. Призначено для ситуацій, коли читання здійснюється частіше, ніж запис.
Лок на запис: Цей лок може мати лише один потік одночасно(exclusive). Якщо потік має лок на запис, жоден інший потік (чи це читання, чи запис) не може отримати лок на читання або запис. Це гарантує ексклюзивний доступ до ресурсу для потоку, який записує.
2. Внутрішня структура ReentrantReadWriteLock
a. Стан:
Внутрішній стан ReentrantReadWriteLock підтримується як єдина змінна типу int. Це число розділено на дві частини:
Верхні 16 бітів для кількості локів на читання (всіми потоками).
Нижні 16 бітів для кількості локів на запис (лічильник повторного входження для потоку запису).
b. Лічильник утримань (Holding count):
Коли потік отримує лок на запис кілька разів (повторно), нижні 16 бітів (частина запису) збільшуються.
Коли потік отримує лок на читання, верхні 16 бітів збільшуються. Але є нюанс: так як декілька потоків можуть мати лок на читання одночасно, ReadWriteLock повинен відстежувати, скільки локів на читання має кожен потік. Для цього використовується ThreadLocalHoldCounter, яка відображає потоки на їхні лічильники. Верхні 16 бітів представляють суму всіх цих лічильників.
Приклад. Потік займає лок на читання:
Read count: 0000 0000 0000 0001
Write count: 0000 0000 0000 0000
c. Взяття і відпускання локу (Acquiring/releasing):
Коли потік намагається отримати лок на читання, він перевіряє, чи має інший потік лок на запис (перевіряючи нижні 16 бітів). Якщо ні, він збільшує лічильник локів на читання.
Коли потік намагається отримати лок на запис, він перевіряє лічильники локів на читання та запису. Лок на запис можна отримати, якщо жоден потік не має локу на читання (верхні 16 бітів нульові) і якщо лок на запис не має або має викликаючий потік (повторне отримання).
При відпусканні відповідні лічильники зменшуються.
d. Справедливість (Fairness):
ReentrantReadWriteLock може працювати в двох режимах: справедливому та несправедливому.
У справедливому режимі потоки отримують доступ в порядку, в якому вони хотіли отримати лок. Це досягається за допомогою внутрішньої черги очікування. Це зменшує шанси на голодування потоків, але часто має меншу пропускну спроможність, ніж в несправедливому режимі.
У несправедливому режимі є так звана поведінка “вривання“. Якщо потік хоче читати, і є інші очікуючі потоки, але жоден з них не є “письмовим“, він може "вриватися" і отримувати лок еа читання. Це може підвищити продуктивність, але може вводити деяку непередбачуваність у планування потоків.
e. Умови (Condition):
Лок на запис (і лише на запис) надає об'єкти Condition, які дозволяють потокам "очікувати" на конкретні умови для їх виконання. Це не підтримується для локів на читання через потенційні дедлоки.
3. Для чого використовувати?
Головною перевагою ReadWriteLock є “покращена паралельність” під час інтенсивних запитів на читання. Дозволяючи кільком потокам читати одночасно, він часто забезпечує кращу продуктивність, ніж синхронізований блок, особливо коли операції запису виконуються не часто.
Взяття локу на запис
public class Main { public static void main(String[] args) { ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); Thread thread1 = new MyThread(readWriteLock, "One"); Thread thread2 = new MyThread(readWriteLock, "Two"); } } class MyThread extends Thread { private final ReadWriteLock readWriteLock; public MyThread(ReadWriteLock readWriteLock, String name) { setName(name); this.readWriteLock = readWriteLock; start(); } @Override public void run() { try { System.out.println(getName() +": About to acquire the lock"); readWriteLock.writeLock().lock(); System.out.println(getName() +": Took the lock. Doing some operations with it"); while (true); } finally { readWriteLock.writeLock().unlock(); } } }
Взяття локу на читання (змінився тільки метод run)
@Override public void run() { try { System.out.println(getName() +": About to acquire the lock"); readWriteLock.readLock().lock(); System.out.println(getName() +": Took the lock. Doing some operations with it"); while (true); } finally { readWriteLock.readLock().unlock(); } }