Java for Beginner
675 subscribers
548 photos
155 videos
12 files
839 links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Cинхронизированные коллекции, их внутреннее устройство и особенности

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

Cозданиe синхронизированных коллекций

Один из наиболее распространенных методов — использование класса Collections, который предлагает методы для обертывания стандартных коллекций в их синхронизированные версии.

Использование Collections.synchronizedXXX():

Класс Collections предоставляет статические методы для создания синхронизированных коллекций:

List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Set<Integer> synchronizedSet = Collections.synchronizedSet(new HashSet<>());
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
Эти методы оборачивают оригинальные коллекции в специальные прокси-объекты, которые обеспечивают синхронизацию доступа к ним.


CopyOnWriteArrayList и CopyOnWriteArraySet:
Помимо использования Collections.synchronizedXXX(), в Java также доступны коллекции, специально разработанные для использования в многопоточных средах. Например, CopyOnWriteArrayList и CopyOnWriteArraySet — это коллекции, которые создают копию набора данных при каждом изменении.
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
Set<String> copyOnWriteSet = new CopyOnWriteArraySet<>();
Эти коллекции оптимизированы для сценариев, где частое чтение данных значительно превосходит частоту их изменения, так как они избегают блокировок при чтении.


ConcurrentHashMap:
Для синхронизированных ассоциативных массивов (Map) Java предлагает ConcurrentHashMap, который представляет собой высокоэффективную и масштабируемую альтернативу синхронизированному HashMap. ConcurrentHashMap разделяет коллекцию на сегменты и блокирует их независимо, что повышает производительность в условиях высокой конкуренции.
Map<Integer, String> concurrentMap = new ConcurrentHashMap<>();
В отличие от Collections.synchronizedMap(), ConcurrentHashMap предоставляет более гибкие методы для работы с параллельными потоками, такие как putIfAbsent(), replace() и computeIfAbsent().


Внутреннее устройство синхронизированных коллекций


Синхронизация доступа:
Методы синхронизированных коллекций, созданных с помощью Collections.synchronizedXXX(), обернуты в блоки synchronized, которые обеспечивают монопольный доступ к коллекции для каждого потока:
public static <T> List<T> synchronizedList(List<T> list) {
return new SynchronizedList<>(list);
}

static class SynchronizedList<E> implements List<E> {
private final List<E> list;

public synchronized boolean add(E e) {
synchronized (this) {
return list.add(e);
}
}

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


#Java #Training #Medium #SynchronizedCollections
Реализация CopyOnWriteArrayList и CopyOnWriteArraySet:
CopyOnWriteArrayList и CopyOnWriteArraySet реализованы таким образом, что при добавлении или удалении элементов создается новая копия коллекции. Это обеспечивает потокобезопасное чтение, но при этом имеет некоторую стоимость в виде дополнительной памяти и времени на копирование данных.
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
Здесь используется ReentrantLock для блокировки доступа при изменении коллекции, а чтение происходит без блокировок, что делает коллекцию чрезвычайно эффективной при большом количестве операций чтения.


Реализация ConcurrentHashMap:

ConcurrentHashMap основан на концепции сегментирования, где весь набор данных разделяется на сегменты, каждый из которых может быть заблокирован независимо. Это позволяет нескольким потокам одновременно модифицировать разные сегменты карты без взаимной блокировки.
static class Segment<K,V> extends ReentrantLock {
volatile HashEntry<K,V>[] table;
// Методы для работы с сегментами карты
}
Каждая операция, которая изменяет данные, блокирует только соответствующий сегмент, что значительно увеличивает параллелизм и производительность по сравнению с глобальной блокировкой.


Особенности синхронизированных коллекций


Производительность:
Синхронизированные коллекции обеспечивают безопасность потоков, но иногда это происходит за счет производительности. Частая блокировка может привести к увеличению времени ожидания для потоков, что особенно заметно в условиях высокой конкуренции. В таких случаях, специализированные коллекции, такие как ConcurrentHashMap или CopyOnWriteArrayList, могут предложить лучшие решения.

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

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

Совместимость с другими частями Java Collection Framework:
Синхронизированные коллекции полностью совместимы с остальной частью Java Collection Framework, что позволяет использовать их вместе с другими коллекциями и утилитами, такими как сортировка или фильтрация.

Ссылки на полезные статьи (спасибо авторам за проделанную работу) :
https://javarush.com/groups/posts/23615-kofe-breyk-278-chto-takoe-sinkhronizirovannihe-kollekcii-v-java-i-kak-oni-rabotajut-razlichija
https://for-each.dev/lessons/b/-java-synchronized-collections

#Java #Training #Medium #SynchronizedCollections
Примеры использования синхронизированных коллекций

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

Мы можем использовать Collections.synchronizedMap() для создания потокобезопасной карты, где ключами будут идентификаторы клиентов, а значениями — списки с историей запросов.
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;

public class RequestHistoryManager {
private final Map<String, List<String>> clientRequestHistory = Collections.synchronizedMap(new HashMap<>());

public void addRequest(String clientId, String request) {
clientRequestHistory.computeIfAbsent(clientId, k -> Collections.synchronizedList(new ArrayList<>())).add(request);
}

public List<String> getRequestHistory(String clientId) {
return clientRequestHistory.getOrDefault(clientId, new ArrayList<>());
}
}

// Пример использования
RequestHistoryManager manager = new RequestHistoryManager();

manager.addRequest("client1", "GET /index.html");
manager.addRequest("client1", "POST /login");
manager.addRequest("client2", "GET /products");

System.out.println(manager.getRequestHistory("client1"));
System.out.println(manager.getRequestHistory("client2"));
В этом примере clientRequestHistory представляет собой синхронизированную карту, а списки запросов для каждого клиента также являются синхронизированными. Это гарантирует, что добавление и получение истории запросов будет происходить корректно даже при параллельном доступе.


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

Мы можем использовать Collections.synchronizedList() для создания потокобезопасной очереди заданий. Для обработки заданий можно использовать несколько потоков, каждый из которых будет извлекать задания из очереди и обрабатывать их.
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class TaskQueueManager {
private final List<String> taskQueue = Collections.synchronizedList(new LinkedList<>());

public void addTask(String task) {
taskQueue.add(task);
}

public String getNextTask() {
if (taskQueue.isEmpty()) {
return null;
}
return taskQueue.remove(0);
}
}

// Пример использования
TaskQueueManager taskQueueManager = new TaskQueueManager();

// Добавление задач
taskQueueManager.addTask("Task 1");
taskQueueManager.addTask("Task 2");
taskQueueManager.addTask("Task 3");

// Потоки обработки задач
Runnable worker = () -> {
String task;
while ((task = taskQueueManager.getNextTask()) != null) {
System.out.println(Thread.currentThread().getName() + " processing " + task);
}
};

Thread worker1 = new Thread(worker);
Thread worker2 = new Thread(worker);

worker1.start();
worker2.start();
Здесь taskQueue — это синхронизированный список, используемый как очередь задач. Потоки worker1 и worker2 одновременно извлекают задания из этой очереди и обрабатывают их. Благодаря синхронизации, мы гарантируем, что каждое задание будет обработано только одним потоком.


#Java #Training #Medium #SynchronizedCollections
Пример 3: Счетчик посещений веб-сайта
Представим, что у нас есть веб-сайт, который обрабатывает множество запросов в секунду. Нам нужно вести подсчет посещений сайта, причем из разных потоков одновременно. В этом случае синхронизированные коллекции помогут избежать некорректного подсчета.

Мы можем использовать Collections.synchronizedMap() для создания потокобезопасной карты, где ключом будет страница, а значением — счетчик посещений.

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class VisitCounter {
private final Map<String, Integer> visitCounts = Collections.synchronizedMap(new HashMap<>());

public void recordVisit(String page) {
visitCounts.merge(page, 1, Integer::sum);
}

public int getVisitCount(String page) {
return visitCounts.getOrDefault(page, 0);
}
}

// Пример использования
VisitCounter visitCounter = new VisitCounter();

// Моделируем посещения страниц
Runnable visitSimulator = () -> {
for (int i = 0; i < 1000; i++) {
visitCounter.recordVisit("/index.html");
visitCounter.recordVisit("/contact.html");
}
};

Thread thread1 = new Thread(visitSimulator);
Thread thread2 = new Thread(visitSimulator);

thread1.start();
thread2.start();

try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("Index Page Visits: " + visitCounter.getVisitCount("/index.html"));
System.out.println("Contact Page Visits: " + visitCounter.getVisitCount("/contact.html"));
В этом примере мы используем synchronizedMap для подсчета посещений страниц. Метод merge() безопасен для использования в многопоточной среде, что позволяет корректно увеличивать счетчики.


Пример 4: Синхронизированный доступ к общему ресурсу

Рассмотрим задачу, в которой несколько потоков должны безопасно добавлять элементы в общий список и извлекать их для обработки. Важно, чтобы все операции были синхронизированы, чтобы избежать потери данных.

Задача: Создать потокобезопасную очередь задач.
import java.util.Queue;
import java.util.LinkedList;
import java.util.Collections;

public class SynchronizedTaskQueue {
private final Queue<String> taskQueue = Collections.synchronizedList(new LinkedList<>());

public void addTask(String task) {
synchronized (taskQueue) {
taskQueue.add(task);
taskQueue.notify(); // Уведомляем ждущий поток о новой задаче
}
}

public String getTask() throws InterruptedException {
synchronized (taskQueue) {
while (taskQueue.isEmpty()) {
taskQueue.wait(); // Ждем, пока появится новая задача
}
return taskQueue.poll();
}
}
}

// Пример использования
SynchronizedTaskQueue queue = new SynchronizedTaskQueue();

// Поток для добавления задач
Thread producer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
queue.addTask("Task " + i);
}
});

// Поток для обработки задач
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
String task = queue.getTask();
System.out.println("Processing " + task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

producer.start();
consumer.start();
В этом примере мы используем синхронизированную очередь задач, где метод addTask() добавляет задачи в очередь и уведомляет ждущие потоки, а метод getTask() ждет появления новой задачи, если очередь пуста. Таким образом, обеспечивается безопасный доступ к общему ресурсу в многопоточной среде.


#Java #Training #Medium #SynchronizedCollections