JavaRush /Java блог /Архив info.javarush /Уровень 26. Ответы на вопросы к собеседованию по теме уро...
zor07
31 уровень
Санкт-Петербург

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

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

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

Concurrency – это библиотека классов в Java, в которой собрали специальные классы, оптимизированные для работы из нескольких нитей. Эти классы собраны в пакете java.util.concurrent. Их можно схематично поделить по функциональному признаку следующим образом: Уровень 26. Ответы на вопросы к собеседованию по теме уровня. Часть 2. Вопросы 6-9, 11-12 - 2Concurrent 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. 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.

  5. Итого

    Итак, основные преимущества и особенности реализации ConcurrentHashMap:

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

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 заменяет его тремя кусками кода:
  1. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
  2. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
  3. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.
Часть 1
Комментарии (13)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Руслан Уровень 31
25 марта 2020
В спецификации языка Java вообще ни слова о мьютексах. В пункте 17.1 сказано
Each object in Java is associated with a monitor, which a thread can lock or unlock. Only one thread at a time may hold a lock on a monitor.

С каждым объектом в Java связан с монитором, который может быть захвачен или освобожден потоком. Только один поток в текущий момент может владеть монитором.
Что то похожее написано про мьютексы в википедии. Складывается ощущение что это одно и то же.
Shamil Уровень 41
20 февраля 2020
К примеру по Lock API. Полагаю, проверку выполнения условия и начало try-catch блока надо поменять местами. Пример из спецификации Lock lock = ...; if (lock.tryLock()) { try { // manipulate protected state } finally { lock.unlock(); } } else { // perform alternative actions }
hidden #1281202 Уровень 41
11 апреля 2019
Что бы никто не запутался названием вопроса выше: Lock - интерфейс, реализации которого применяются в качестве более функциональной альтернативы оператору synchronized. В том числе, ранее в задаче, можно было наблюдать наиболее известную имплементацию Lock`a, ReentrantLock. Так что 9-ый вопрос надо поставить не так: "Что такое класс Lock?", а "Что такое интерфейс Lock?"
pautina Уровень 33
24 июня 2017
if(lock.tryLock(10, TimeUnit.SECONDS)){
            resource.doSomething();
            }

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

а так будет равносильно:
lock.lock() {
            resource.doSomething();
        }finally{
    lock.unLock()
}
lichMax Уровень 40
29 апреля 2017
Неплохая подборка ответов на вопросы. Только я бы из ответа на первый вопрос перенёс в ответ на второй вопрос схема группировки классов Конкаренси, а также описание этих групп.
NemchinovSergey Уровень 40
22 февраля 2017
Когда мы пишем блок synchronized, то компилятор Java заменяет его тремя кусками кода:

Я бы это переписал вот так:
1. Перед блоком synchronized добавляется код, который смотрит, если мютекс занят – то нить должна ждать его освобождения.
2. В начале блока synchronized добавляется код, который отмечает мютекс как занятый.
3. В конце блока synchronized добавляется код, который отмечает мютекс как свободный.
Eli_Ver Уровень 27
30 января 2017
В примере некорректный комментарий:
// лочим на 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.
HOS Уровень 40
4 ноября 2016
Ответ на 3-й вопрос по ссылке ниже.
spec-zone.ru/RU/Java/Docs/7/api/java/util/Collections.html
Champion Уровень 41
1 сентября 2016
Большое спасибо за предоставленную информацию!!!
Joysi Уровень 41
31 июля 2016
Понятно написано.