Concurrent

https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
31.12.2012https://habr.com/ru/post/164487
16.01.2024https://dev.to/faangmaster/vopros-s-sobiesiedovaniia-chto-takoie-java-memory-model-i-happens-before-410g
Process

Процесс - совокупность кода и данных разделяющих общее виртуальное адресное пространство. При помощи процессов выполнение разных программ изолировано друг от друга: каждое приложение использует свою область памяти, не мешая другим программам. Чаще всего одна программа состоит из одного процесса, но бывают и исключения (например, браузер Chrome создает отдельный процесс для каждой вкладки, что дает ему некоторые преимущества, вроде независимости вкладок друг от друга). Процессы изолированы друг от друга, поэтому прямой доступ к памяти чужого процесса невозможен (взаимодействие между процессами осуществляется с помощью специальных средств).

• Если запущен процесс - в нем запущен хотя бы 1 поток.

• Процесс завершается когда завершается его последний поток.

Thread

Поток - небольшая часть процесса, последовательность инструкций, которая выполняется параллельно с другими потоками. Каждое приложение создает как минимум один поток: основной, который запускает функцию main(). Один поток – это одна единица исполнения кода. Каждый поток последовательно выполняет инструкции процесса, которому он принадлежит, параллельно с другими потоками этого процесса.

Java Memory Model

Модель памяти Java. Часть семантики языка. Описывает поведение потоков в среде исполнения Java. Определяет правила и гарантии, касающиеся порядка доступа к переменным из различных потоков. JMM определена в терминах actions (действий) - чтение и запись в переменные, lock и unlock мониторов, запуск start и join потоков.

• Однопоточные программы исполняются псевдопоследовательно. Это значит: в реальности процессор может выполнять несколько операций за такт, заодно изменив их порядок, однако все зависимости по данным остаются, так что поведение не отличается от последовательного.

• Нет невесть откуда взявшихся значений. Чтение любой переменной (кроме не-volatile long и double, для которых это правило может не выполняться) выдаст либо значение по умолчанию (ноль), либо что-то, записанное туда другой командой.

• Остальные события выполняются по порядку, если связаны отношением строгого частичного порядка «выполняется прежде» (happens before).

Heap (Куча)

Часть памяти которая хранит фактические объекты (экземпляры классов).

• Создание нового объекта происходит в куче.

• Любой объект, созданный в куче, имеет глобальный доступ и на него могут ссылаться из любой части приложения.

• Куча это огромный объем памяти по сравнению со стеком.

• Если память кучи заполнена вызывается OutOfMemoryError.

• Максимальный размер кучи не определен заранее - зависит от работающей JVM машины. обычно это вся доступная оперативная память (~500 MB).

Stack

Стековая память отвечает за хранение ссылок на объекты кучи и за хранение типов значений (примитивные типы в Java) которые содержат само значение а не ссылку на объект кучи.

• Работает по схеме LIFO (Last In - First Out).

• Стековая память в Java выделяется для каждого потока. каждый раз когда поток создается и запускается он имеет свою собственную стековую память и не может получить доступ к стековой памяти другого потока.

• Из-за простоты распределения памяти стековая память работает намного быстрее кучи.

• Если память стека полностью занята - вызывается StackOverflowError.

• Максимальный размер стека не определен заранее - зависит от работающей JVM машины. обычно это ~1 MB.

Разница между Stack и Heap?

• Куча используется всеми частями приложения в то время как стек используется только одним потоком исполнения программы.

• Всякий раз, когда создается объект, он всегда хранится в куче, а в памяти стека содержится ссылка на него. Память стека содержит только локальные переменные примитивных типов и ссылки на объекты в куче.

• Объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков.

• Управление памятью в стеке осуществляется по схеме LIFO.

• Стековая память существует лишь какое-то время работы программы, а память в куче живет с самого начала до конца работы программы.

• Если память стека полностью занята, то Java Runtime бросает java.lang.StackOverflowError, а если память кучи заполнена, то бросается исключение java.lang.OutOfMemoryError: Java Heap Space.

• Размер памяти стека намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.

Happens before

JMM задает частичный порядок, который называется happens-before, для всех действий (actions) в рамках программы. Для того, чтобы гарантировать, что поток, который выполняет действие B может видеть результат действия A (в не зависимости A и B выполняются в одном потоке или нет), необходимо, чтобы существовала happens-before связь между A и B. Если такой связи нет, то JVM может выполнять действия A и B в произвольном порядке. И никаких гарантий видимости не накладывается. Отношение happens-before является отношением частичного порядка между двумя операциями. Если одна операция происходит-до другой, то ее результат видим и упорядочен для другой.

Runnable

Представляет собой функциональный интерфейс, который содержит единственный метод run(), предназначенный для выполнения кода в отдельном потоке. Он часто используется для создания потоков, позволяя передавать экземпляры классов, реализующих этот интерфейс, в конструкторы классов Thread или ExecutorService. Он не возвращает результат и не может выбрасывать проверяемые исключения.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello from a thread!");
    }
}

public static void main(String[] args) {
    MyRunnable myRunnable = new MyRunnable();
    Thread thread = new Thread(myRunnable);
    thread.start();
}
Callable

Функциональный интерфейс, который возвращает результат и может выбрасывать проверяемые исключения. Он используется с ExecutorService.

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() {
        return 42;
    }
}

// Использование
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
try {
    Integer result = future.get(); // Получение результата
    System.out.println("Result from Callable: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
} finally {
    executor.shutdown();
}
Method Area

В области Method area хранится скомпилированный код для каждой функции. Когда поток начинает выполнять функцию, он получает инструкции из этой области. По сути, она представляет собой кулинарную книгу рецептов, где подробно описано, как что приготовить.

ThreadPool

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

• Повторное использование потоков. В ThreadPool потоки создаются один раз и повторно используются для выполнения множества задач, что снижает затраты на создание и уничтожение потоков.

• Управление количеством потоков. ThreadPool позволяет ограничить количество потоков, которые одновременно выполняются, чтобы избежать чрезмерной нагрузки на систему.

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

fun main() {
    // Создание пула с фиксированным количеством потоков
    val executorService = Executors.newFixedThreadPool(3)
    
    // Отправка задач в пул
    repeat(10) { taskId ->
        executorService.submit {
            println("Task $taskId is running on ${Thread.currentThread().name}")
            Thread.sleep(2000) // Имитация работы
        }
    }
    
    // Завершение работы пула и ожидание завершения всех задач
    executorService.shutdown()
}
FixedThreadPool

Создает фиксированное количество потоков. Если все потоки заняты, новые задачи помещаются в очередь до тех пор, пока не освободится один из потоков.

CachedThreadPool

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

SingleThreadExecutor

Выполняет задачи последовательно в одном потоке. Подходит для задач, которые должны быть выполнены последовательно.

ScheduledThreadPool

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

Executor

Интерфейс для запуска задач в многопоточной среде. Вместо явного создания потоков задачи передаются в метод execute(), а управление потоками берёт на себя реализация Executor.

public class ExecutorExample {
    public static void main(String[] args) {
        // Создаём пул из фиксированного количества потоков
        Executor executor = Executors.newFixedThreadPool(3);

        // Отправляем задачи на выполнение
        for (int i = 1; i <= 5; i++) {
            int taskNumber = i;
            executor.execute(() -> {
                System.out.println("Task " + taskNumber + " is running on thread: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // Симуляция работы задачи
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
    }
}
ExecutorService

Расширение интерфейса Executor, которое добавляет управление жизненным циклом пула потоков. Он позволяет запускать задачи асинхронно, получать их результаты через Future и управлять завершением работы потоков с методами shutdown() и awaitTermination().

public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        executorService.submit(() -> System.out.println("Task 1 on thread: " + Thread.currentThread().getName()));
        executorService.submit(() -> System.out.println("Task 2 on thread: " + Thread.currentThread().getName()));

        executorService.shutdown();
    }
}
ScheduledExecutorService

Интерфейс для запуска задач с задержкой или по расписанию. Это современная альтернатива Timer, которая позволяет запускать задачи однократно через заданный интервал или периодически. Исключения в одной задаче не влияют на выполнение других.

public class ScheduledExecutorExample {
    public static void main(String[] args) {
        // Создаём планировщик с пулом из двух потоков
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // Задача, выполняемая один раз через 3 секунды
        scheduler.schedule(() -> {
            System.out.println("Task executed after 3 seconds");
        }, 3, TimeUnit.SECONDS);

        // Задача с фиксированным интервалом: каждые 2 секунды, начиная через 1 секунду
        scheduler.scheduleAtFixedRate(() -> {
            System.out.println("Periodic task (fixed rate): " + System.currentTimeMillis());
        }, 1, 2, TimeUnit.SECONDS);

        // Задача с задержкой между завершением одной и началом следующей: 2 секунды
        scheduler.scheduleWithFixedDelay(() -> {
            System.out.println("Periodic task (fixed delay): " + System.currentTimeMillis());
            try {
                // Симулируем работу задачи на 1.5 секунды
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, 1, 2, TimeUnit.SECONDS);

        // Завершаем планировщик через 10 секунд
        scheduler.schedule(() -> {
            System.out.println("Shutting down scheduler");
            scheduler.shutdown();
        }, 10, TimeUnit.SECONDS);
    }
}
ThreadPoolExecutor

Инструмент для управления пулом потоков и выполнения задач в многопоточной среде. Он автоматически масштабирует количество потоков в зависимости от нагрузки, а задачи, которые не могут быть выполнены сразу, помещаются в очередь. Поддерживает настройку минимального и максимального количества потоков, очереди задач, таймаутов для завершения неактивных потоков и обработчиков отклонённых задач. Исключения в одной задаче не влияют на выполнение других. Используется для создания кастомизированных пулов потоков с высокой производительностью.

public class ThreadPoolExecutorExample {
    public static void main(String[] args) {
        // Создаём кастомный ThreadPoolExecutor
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,                          // Минимальное количество потоков (corePoolSize)
                4,                          // Максимальное количество потоков (maximumPoolSize)
                60,                         // Таймаут для завершения неактивных потоков (keepAliveTime)
                TimeUnit.SECONDS,           // Единицы времени таймаута
                new LinkedBlockingQueue<>(2) // Очередь задач
        );

        // Добавляем задачи в пул
        for (int i = 1; i <= 6; i++) {
            int taskNumber = i;
            executor.execute(() -> {
                System.out.println("Task " + taskNumber + " is running on thread: " + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000); // Симуляция работы задачи
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // Завершаем пул после выполнения всех задач
        executor.shutdown();
    }
}
ScheduledThreadPoolExecutor

Инструмент для запуска задач с задержкой или по расписанию. Он работает на базе пула потоков, что позволяет выполнять задачи параллельно и безопасно в многопоточной среде.

public class ScheduledExecutorExample {
    public static void main(String[] args) {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);

        // Задача с задержкой в 2 секунды
        executor.schedule(() -> {
            System.out.println("Task executed after 2 seconds");
        }, 2, TimeUnit.SECONDS);

        // Периодическая задача каждые 3 секунды
        executor.scheduleAtFixedRate(() -> {
            System.out.println("Periodic task: " + System.currentTimeMillis());
        }, 0, 3, TimeUnit.SECONDS);

        // Завершение работы через 10 секунд
        executor.schedule(() -> {
            executor.shutdown();
            System.out.println("Executor shutdown");
        }, 10, TimeUnit.SECONDS);
    }
}
Future

Интерфейс описывает API для работы с задачами, результат которых мы планируем получить в будущем: методы получения результата, методы проверки статуса.

Callable task = () -> {
    return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
synchronized

Если метод или блок кода помечен ключевым словом synchronized это значит, что блок может выполняться только одним потоком одновременно. Позволяет заблокировать доступ к методу или части кода, если его уже использует другой поток. Если метод синхронизирован то вся логика внутри него выполняется одним потоком одновременно. Если один поток зашел внутрь блока кода, который помечен словом synchronized, он моментально захватывает мьютекс объекта, и все другие потоки, которые попытаются зайти в этот же блок или метод вынуждены ждать, пока предыдущий поток не завершит свою работу и не освободит монитор.
• Если в
synchronized блоке возникнет исключение - блокировка будет снята.

public synchronized void doSomething() {
    // целый синхронизированный метод
}
private Object obj = new Object();

public void doSomething() {
    //...логика, доступная для всех потоков
    synchronized (obj) {
        // логика, которая одновременно доступна только для одного потока
    }
}
volatile

Модификатор, который используется для переменных, чтобы гарантировать их правильное чтение и запись в многопоточной среде. Изменения volatile переменной в одном потоке сразу видны всем другим потокам. Это достигается тем, что операции чтения и записи происходят непосредственно из/в основную память, а не через кеш процессора. Операции над volatile (например, count++) не являются атомарными.

class Example {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // выполнение кода
        }
    }

    public void stop() {
        running = false; // изменения сразу станут видны в потоке run()
    }
}
mutex

«MUTual EXclusion» — «взаимное исключение». Специальный объект для синхронизации потоков. Прикреплен к каждому объекту в Java. Задача мьютекса — обеспечить такой механизм, чтобы доступ к объекту в определенное время был только у одного потока. Если Поток-1 захватил мьютекс объекта А, остальные потоки не получат к нему доступ, чтобы что-то в нем менять. До тех пор, пока мьютекс объекта А не освободится, остальные потоки будут вынуждены ждать. Мьютекс - мячик, говорит тот у кого он в руках. Он уже встроен в класс Object а значит, есть у каждого объекта в Java.

Монитор

Надстройка над мьютексом. Невидимый кусок кода, обеспечивающий защитный механизм объекта от других потоков. Монитор в Java выражен с помощью слова synchronized.

Semaphore

Семафор - средство для синхронизации доступа к какому-то ресурсу. При создании механизма синхронизации он использует счетчик (сколько потоков одновременно могут получать доступ к общему ресурсу).

permits - начальное и максимальное значение счетчика. То есть то, сколько потоков одновременно могут иметь доступ к общему ресурсу.

fair - для установления порядка, в котором потоки будут получать доступ. Если fair = true, доступ предоставляется ожидающим потокам в том порядке, в котором они его запрашивали. Если же он равен false, порядок будет определять планировщик потоков.

acquire - запрашивает разрешение на доступ к ресурсу у семафора. Если счетчик > 0, разрешение предоставляется, а счетчик уменьшается на 1.

release - «освобождает» выданное ранее разрешение и возвращает его в счетчик (увеличивает счетчик разрешений семафора на 1).

Semaphore(int permits)
Semaphore(int permits, boolean fair)

Semaphore semaphore = new Semaphore(2);
semaphore.acquire();
semaphore.release();
ThreadLocal

Класс который хранит локальные переменные для потоков. Эти переменные изолированы между разными потоками и могут быть доступны только своему собственному потоку.

• Изоляция данных между потоками.

• Управление сессиями для подключения к базе данных.

• Хранение транзакционной информации потока.

ThreadLocal<String> local = new ThreadLocal<>();
ReentrantLock

Класс, который предоставляет механизм блокировки потоков с возможностью повторного захвата. Это улучшенная альтернатива традиционным синхронизированным методам (использующим synchronized) и предоставляет больше гибкости в управлении потоками и синхронизации.

• Позволяет одному и тому же потоку захватывать блокировку несколько раз. Это удобно, если поток уже владеет блокировкой и нужно выполнить дополнительный захват, например, при вызове вложенного метода, который также требует блокировки.

ReentrantLock lock = new ReentrantLock();

void method() {
    lock.lock();
    try {
        // Внутренний код
        nestedMethod();
    } finally {
        lock.unlock();
    }
}

void nestedMethod() {
    lock.lock(); // Можно захватить блокировку снова
    try {
        // Внутренний код
    } finally {
        lock.unlock(); // Нужно разблокировать все захваты
    }
}

• Предоставляет методы для явного захвата и освобождения блокировки, что позволяет лучше контролировать процесс синхронизации.

ReentrantLock lock = new ReentrantLock();

lock.lock(); // Захват блокировки
try {
    // Критическая секция
} finally {
    lock.unlock(); // Освобождение блокировки
}

• Поддерживает создание условных переменных через newCondition, что позволяет реализовывать более сложные сценарии синхронизации, такие как ожидание и уведомление.

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

void waitMethod() throws InterruptedException {
    lock.lock();
    try {
        condition.await(); // Ожидание сигнала
    } finally {
        lock.unlock();
    }
}

void signalMethod() {
    lock.lock();
    try {
        condition.signal(); // Отправка сигнала
    } finally {
        lock.unlock();
    }
}

• Позволяет захватывать блокировку с таймаутом через методы tryLock и tryLock(long, TimeUnit)

ReentrantLock lock = new ReentrantLock();
boolean acquired = lock.tryLock(1000, TimeUnit.MILLISECONDS); // Попытка захвата блокировки с таймаутом
if (acquired) {
    try {
        // Критическая секция
    } finally {
        lock.unlock();
    }
}

• Может быть создан с флагом справедливости. Если блокировка создаётся с true для параметра fair, то она будет соблюдать порядок захвата: потоки будут захватывать блокировку в порядке их запроса.

ReentrantLock fairLock = new ReentrantLock(true); // Справедливая блокировка
ReadWriteLock

Интерфейс в Java, который обеспечивает механизм синхронизации, позволяющий нескольким потокам одновременно читать данные, но ограничивает доступ к данным для записи, чтобы избежать конфликтов. Он используется для повышения производительности, когда в основном операции чтения более частые, чем операции записи.

• Разделение на чтение и запись. ReadWriteLock имеет два типа блокировок: ReadLock и WriteLock.

class SharedResource {
    private val lock = ReentrantReadWriteLock()
    private var data: Int = 0

    fun readData(): Int {
        lock.read { // захватываем блокировку на чтение
            return data
        }
    }

    fun writeData(newData: Int) {
        lock.write { // захватываем блокировку на запись
            data = newData
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Чтение из нескольких потоков
    val readerThread1 = Thread { println("Reader 1: ${resource.readData()}") }
    val readerThread2 = Thread { println("Reader 2: ${resource.readData()}") }

    // Запись в одном потоке
    val writerThread = Thread { resource.writeData(10) }

    // Запускаем потоки
    readerThread1.start()
    readerThread2.start()
    writerThread.start()

    readerThread1.join()
    readerThread2.join()
    writerThread.join()
}

ReadLock

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

WriteLock

Запись. Позволяет только одному потоку записывать данные. Если поток захватывает блокировку на запись, другие потоки не могут ни читать, ни записывать данные.

CountDownLatch

Это инструмент синхронизации в Java, который используется, чтобы заставить один или несколько потоков ждать завершения набора операций в других потоках. Например, вы хотите запустить несколько задач параллельно и дождаться их завершения, прежде чем продолжить основную работу.

CountDownLatch создается с целым числом, которое обозначает количество операций или событий, которые должны произойти, чтобы latch (защелка) была “отпущена”.

CountDownLatch latch = new CountDownLatch(3); // ждем завершения 3 операций

• Несколько потоков выполняют свои задачи и в конце каждой из них вызывают метод countDown(), чтобы сообщить, что одна операция завершена.

latch.countDown(); // уменьшает счетчик на 1

• Главный поток или другой поток вызывает метод await(), который блокирует его выполнение до тех пор, пока счётчик не станет равен нулю (все задачи завершены).

latch.await(); // ждет, пока счетчик не станет 0
CyclicBarrier

Это инструмент синхронизации в Java, который позволяет группе потоков ожидать друг друга, чтобы они могли начать выполнение одновременно. Это полезно, когда нужно, чтобы несколько потоков встретились в определённой точке программы (барьере), а затем все одновременно продолжили выполнение. В отличие от CountDownLatch, барьер можно использовать многократно. Как только все потоки достигают барьера, он «сбрасывается», и потоки могут снова встретиться в следующей точке программы.

CyclicBarrier инициализируется с числом, которое указывает, сколько потоков должно достигнуть барьера для его “отпуска”.

CyclicBarrier barrier = new CyclicBarrier(3); // Ждем 3 потока

• Каждый поток, когда достигает определённой точки программы, вызывает метод await(). Поток блокируется и ждет, пока все остальные потоки не вызовут await().

barrier.await(); // Ожидает, пока все потоки не соберутся

• Когда все потоки достигают барьера, они продолжают выполнение. При этом CyclicBarrier можно переиспользовать, т.е. он может быть «сброшен» и снова ждать потоков.

deadlock

Взаимная блокировка. Возникает только в случае неверного порядка синхронизации. Разрешить deadlock нельзя. Чтобы предотвратить блокировку:

• Использовать метод Thread.join с максимальным временем, которое вы хотите ждать, чтобы поток завершился.

• Использовать упорядочивание блокировок.

• Избегать ненужных блокировок.

Race condition

Гонка потоков. Если из разных потоков менять одно и тоже поле класса, то это небезопасно и нельзя гарантировать, что они будут изменены в том же порядке, что и задумано. Это называется состояние гонки.

Concurrent Collections

Потокобезопасные коллекции при итерировании не бросают ConcurrentModificationException.

synchronizedList

Потокобезопасный List.

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedSet

Потокобезопасный Set.

Set<String> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
CopyOnWriteArrayList

Потокобезопасный ArrayList.

• Операции add set remove созданиют новую копию внутреннего массива (копируются только ссылки на объекты)

List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArraySet

Имплементация интерфейса Set использующая за основу CopyOnWriteArrayList. В отличии от CopyOnWriteArrayList дополнительных методов нет.

Set<String> set = new CopyOnWriteArraySet<>();
Вопросы на собесе (41)
  • synchronized (8)
    1. Как работает synchronized?

      synchronized обеспечивает эксклюзивный доступ к блоку кода или методу для одного потока за раз, предотвращая одновременный доступ других потоков.

    1. Как synchronized устроен под капотом?

      Реализован через механизм мониторов, которые обеспечивают потокобезопасный доступ к ресурсам. Компилятор генерирует инструкции monitorenter и monitorexit, чтобы запросить и освободить блокировку для указанного объекта, позволяя одному потоку выполнять защищённый код, пока остальные потоки ожидают освобождения блокировки. Этот механизм гарантирует, что только один поток может выполнять синхронизированный код одновременно.

    1. На каком объекте происходит синхронизация статического метода с ключевым словом synchronized?

      На объекте класса.

    1. Что будет являться монитором в synchronized на класс и метод?

      Монитором при использовании synchronized на классе будет сам объект класса (Class), то есть блокировка будет происходить на уровне всего класса. При использовании synchronized на методе, монитором будет являться текущий объект (this), если это нестатический метод, или объект класса, если метод статический.

    1. Что может произойти если не синхронизировать доступ к переменной?

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

    1. Завершит ли программа выполнение при ошибках синхронизации?

      Программа может не завершиться, но её выполнение станет некорректным: возможны зависания, бесконечные циклы или логические ошибки.

    1. Можно ли с помощью synchronized получить deadlock?

      Да, с помощью synchronized можно получить deadlock, если 2 или более потоков пытаются захватить несколько мониторов в разном порядке. Например, поток А захватил объект 1 и ждет объект 2, а поток B захватил объект 2 и ждет объект 1, что приведет к взаимной блокировке (deadlock).

    1. Как отработает функция main?
      public class Incrementer {
      		int count = 0;
      
      		public synchronized void inc() {
      				if (count < 10) {
      						count++;
      						inc();
      				}
      		}
      }
      
      void main() {
      		new Incrementer().inc();
      }

      Этот код приведет к переполнению стека (StackOverflowError). Метод inc рекурсивно вызывает сам себя при условии, что count меньше 10. Поскольку метод инкрементирует count и вызывает себя без окончания вызовов, это приводит к глубокому рекурсивному вызову. При достижении значения count == 10 программа не успеет завершить вызовы из стека, так как все вызовы до этого еще находятся в стеке ожидания завершения.

      Решение — убрать рекурсию или заменить её на итерацию.

  • volatile (4)
    1. Как работает volatile?

      Используется для переменных, доступ к которым происходит из нескольких потоков. Оно гарантирует, что последнее записанное значение будет видимо всем потокам и предотвращает кэширование, заставляя потоки обращаться к основной памяти. Это помогает избежать проблем с видимостью данных, но не обеспечивает атомарности операций.

    1. За счет чего volatile гарантирует happens before?

      За счет того, что запись в volatile переменную завершает все предыдущие операции в текущем потоке, а чтение из нее начинает все последующие операции.

    1. Как то что мы записываем в volatile влияет на кэш потока?

      Запись в volatile переменную влияет на кэш потока, так как она предотвращает кэширование этой переменной. Когда поток записывает значение в volatile, процессор сбрасывает соответствующие данные из кэша в основную память. Это означает, что другие потоки, читающие эту переменную, всегда получат актуальное значение, так как они будут обращаться к основной памяти, а не к кэшу. Таким образом, volatile обеспечивает согласованность данных между потоками, гарантируя, что изменения видны немедленно.

    1. Когда volatile не будет работать?

      volatile гарантирует видимость, но не атомарность. Для сложных операций (например, инкремента) нужен synchronized или атомарные переменные, иначе потоки могут перезаписывать результат.

  • ThreadPool (2)
    1. Что такое ThreadPool и зачем они нужны?

      ThreadPool — это коллекция потоков, управляемая пулом, которая переиспользует потоки для выполнения задач, чтобы избежать накладных расходов на создание и уничтожение потоков. Они нужны для оптимизации использования ресурсов, улучшения производительности и управления многопоточностью.

    1. Почему ThreadPool лучше цикла потоков new Thread?

      Создание и уничтожение потоков для каждой задачи может быть затратным по времени и ресурсам, особенно если задача выполняется быстро и часто. Каждый новый поток требует выделения ресурсов и времени на инициализацию.

      При большом количестве задач может быть создано слишком много потоков, что приведет к большому количеству переключений контекста и снижению производительности.

      Обычные потоки: Не предоставляют встроенного механизма для управления задачами, что может привести к проблемам, если задачи поступают быстрее, чем могут быть выполнены.

      Обычные потоки: Требуется вручную управлять жизненным циклом потоков и обработкой исключений, что может усложнить код.

      Обычные потоки: Обработка исключений и ошибок в каждом потоке может быть трудоемкой и запутанной.

  • Concurrent Collections (3)
    1. Для чего нужны concurrent-коллекции?

      Для безопасной работы с данными из нескольких потоков одновременно. Они предоставляют атомарные операции и механизмы синхронизации, что предотвращает ошибки, такие как состояние гонки и некорректная модификация данных.

    1. Как обеспечить потокобезопасность при работе с коллекциями?

      Для обеспечения потокобезопасности можно использовать синхронизированные коллекции из java.util.Collections или специализированные потокобезопасные коллекции из java.util.concurrent, такие как ConcurrentHashMap или CopyOnWriteArrayList.

    1. Как избежать ConcurrentModificationException?

      Использовать копию коллекции для итерации или применять синхронизацию при доступе к коллекции из нескольких потоков. Также стоит рассмотреть использование потокобезопасных коллекций, таких как CopyOnWriteArrayList или ConcurrentHashMap.

  • Java Memory Model (6)
    1. Как организована модель памяти в JVM?

      Модель памяти JVM организована вокруг концепции разделения на несколько областей: Heap для хранения объектов, Stack для хранения локальных переменных и вызовов методов, и метод Area для хранения байт-кода и метаданных. JVM использует механизм синхронизации, чтобы обеспечить безопасный доступ к общим данным в многопоточных приложениях, а также включает сборщик мусора для управления памятью.

    1. Какие виды памяти есть в Java?

      В Java память делится на стек и кучу.

    1. Что такое Stack и Heap?

      Stack хранит локальные переменные и вызовы функций в порядке LIFO.

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

    1. В чем пренципиальное различие стека и кучи?

      Принципиальное различие между стеком и кучей заключается в том, что стек использует принцип «последний пришёл — первый вышел» (LIFO) для хранения локальных переменных и вызовов функций, тогда как куча предназначена для динамического выделения памяти, где объекты могут создаваться и освобождаться в произвольном порядке.

    1. Какие объекты хранятся в стеке, а какие в куче?

      В стеке хранятся примитивные типы данных (например, int, char, boolean) и ссылки на объекты, а также данные локальных переменных и параметры методов.

      В куче размещаются сами объекты, массивы и экземпляры классов, которые создаются с помощью оператора new.

    1. Какой из разделов памяти JVM является потокобезопасным?

      Стек является потокобезопасным, так как у каждого потока есть свой собственный стек, недоступный другим потокам.

  • deadlock (3)
    1. Что такое deadlock?

      Deadlock — это ситуация, когда два или более потоков блокируют друг друга, ожидая освобождения ресурсов, которые удерживаются другим потоком. В результате ни один из них не может продолжить выполнение, и программа «зависает».

    1. Какие есть способы предотвратить deadlock?

      Чтобы предотвратить deadlock, упорядочивайте захват ресурсов, избегайте циклических зависимостей и используйте тайм-ауты для блокировок.

    1. Опиши ситуацию возникновения взаимной блокировки (deadlock)?

      Возникает, когда два или более потоков или процессов ждут друг друга, чтобы освободить ресурсы, которые они удерживают, и ни один из них не может продолжить выполнение.

  • Race Condition (3)
    1. Что такое Race Condition?

      Это ошибка в многопоточном программировании, когда несколько потоков одновременно изменяют общий ресурс, что приводит к непредсказуемым результатам. Например, если один поток считывает значение переменной, а другой его изменяет, данные могут стать неконсистентными.

    1. Какие есть способы разрешить Race Condition?

      Для разрешения Race Condition можно использовать синхронизацию через ключевое слово synchronized, блокировки через ReentrantLock, атомарные переменные из пакета java.util.concurrent.atomic, и конструкции, такие как CountDownLatch или CyclicBarrier.

    1. В каком случае будет Race Condition?

      Все читают.

      Один пишет, остальные читают.

      Много пишут/много читают.

  • Другие (12)
    1. Какие есть способы синхронизации потоков?

      Способы синхронизации потоков включают использование synchronized, мьютексов, семафоров, и других синхронизирующих примитивов, таких как ReentrantLock, CountDownLatch, CyclicBarrier и ReadWriteLock.

    1. Что такое happens before?

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

    1. Когда кэш одного потока становится доступным для других потоков?

      Кэш попадает в общую память в любой свободный такт процессора.

    1. В чем разница между мьютексом, монитором и семафором?

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

    1. Какие существуют способы синхронизировать доступ к переменной?

      synchronized блокирует доступ к методу или блоку кода для других потоков.

      Lock используется для явного управления блокировкой с методами lock() и unlock().

      Atomic-классы обеспечивают неблокирующий доступ к переменным через атомарные операции (AtomicInteger, AtomicReference и т.д.).

      Volatile обеспечивает видимость изменений переменной между потоками, но не гарантирует атомарность операций.

    1. Разница между Runnable и Callable?

      Runnable не возвращает результат и не может выбрасывать проверяемые исключения, тогда как Callable возвращает результат и может выбрасывать такие исключения. Runnable используется с Thread, а Callable — с ExecutorService.

    1. С какими проблемами можно столкнуться при использовании многопоточности?

      Состояние гонки: Несинхронизированный доступ к общим ресурсам может привести к ошибкам.

      Взаимные блокировки: Потоки могут заблокировать друг друга, ожидая освобождения ресурсов.

      Высокие накладные расходы: Создание и управление потоками может потребовать дополнительных ресурсов.

    1. Есть счетчик A который мы его инкрементим, доступ есть у разных потоков, какое значение будет у счетчика если из 100 потоков вызвать функцию A++?

      Значение счетчика после 100 вызовов A++ может быть непредсказуемым, так как операция инкремента не является атомарной. Разные потоки могут перезаписывать одно и то же значение, из-за чего итоговое значение может оказаться меньше ожидаемого 100.

    1. Разница между функциями инкремента A++ и ++A?

      A++ — постфиксный инкремент, возвращает текущее значение переменной, а затем увеличивает её на 1.

      ++A — префиксный инкремент, сначала увеличивает значение переменной на 1, а затем возвращает новое значение.

    1. Какие результаты увидим?
      var a: Int = 1
      
      println(a++) // 1
      println(++a) // 2
      println(a+1) // 2
    1. Какие результаты увидим?
      var a: Int = 1
      
      println(a++ + 1) // 2
    1. Какие результаты увидим?
      var a: Int = 1
      
      println(++a + 1) // 3