← Назад к вопросам
Как организовать доступ к общим данным в эндпоинте на Spring, чтобы они были разделяемыми между всеми клиентами и потоками
2.3 Middle🔥 181 комментариев
#REST API и микросервисы#Spring Framework#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Общие данные между клиентами в Spring Controllers
1. Синглтон Bean (рекомендуется)
Используем Spring Bean с scope = singleton (по умолчанию):
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
@Component // Singleton по умолчанию
public class SharedDataService {
private final ConcurrentHashMap<String, Object> sharedData = new ConcurrentHashMap<>();
private final AtomicInteger counter = new AtomicInteger(0);
public void setData(String key, Object value) {
sharedData.put(key, value);
}
public Object getData(String key) {
return sharedData.get(key);
}
public int incrementCounter() {
return counter.incrementAndGet();
}
public int getCounter() {
return counter.get();
}
}
@RestController
@RequestMapping("/api/shared")
public class SharedDataController {
private final SharedDataService sharedDataService;
public SharedDataController(SharedDataService sharedDataService) {
this.sharedDataService = sharedDataService;
}
@PostMapping("/data")
public void setData(@RequestParam String key, @RequestParam String value) {
sharedDataService.setData(key, value);
}
@GetMapping("/data/{key}")
public Object getData(@PathVariable String key) {
return sharedDataService.getData(key);
}
@PostMapping("/increment")
public int incrementCounter() {
return sharedDataService.incrementCounter();
}
}
2. Потокобезопасные коллекции
Используем ConcurrentHashMap, AtomicInteger, CopyOnWriteArrayList:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.springframework.stereotype.Service;
@Service
public class ThreadSafeSharedDataService {
// Потокобезопасная коллекция для хранения данных
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
// Потокобезопасный счётчик
private final AtomicInteger requestCount = new AtomicInteger(0);
// Потокобезопасный список
private final CopyOnWriteArrayList<String> events = new CopyOnWriteArrayList<>();
// Потокобезопасный флаг
private final AtomicBoolean isProcessing = new AtomicBoolean(false);
public void addToCache(String key, String value) {
cache.put(key, value);
}
public String getFromCache(String key) {
return cache.get(key);
}
public int getAndIncrementRequestCount() {
return requestCount.incrementAndGet();
}
public void addEvent(String event) {
events.add(event);
}
public boolean setProcessing(boolean value) {
return isProcessing.compareAndSet(!value, value);
}
}
3. Использование Synchronized блоков
Для критических секций:
import org.springframework.stereotype.Service;
@Service
public class SynchronizedSharedDataService {
private java.util.HashMap<String, Object> data = new java.util.HashMap<>();
private int counter = 0;
// Синхронизация для read-write операций
public synchronized void setData(String key, Object value) {
data.put(key, value);
}
public synchronized Object getData(String key) {
return data.get(key);
}
public synchronized int incrementCounter() {
return ++counter;
}
}
4. ReadWriteLock для оптимизации
Если много читателей, мало писателей:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.springframework.stereotype.Service;
@Service
public class ReadWriteSharedDataService {
private final java.util.HashMap<String, String> data = new java.util.HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void setData(String key, String value) {
lock.writeLock().lock();
try {
data.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
public String getData(String key) {
lock.readLock().lock();
try {
return data.get(key);
} finally {
lock.readLock().unlock();
}
}
}
5. Database для долгосрочного хранения
Для персистентных общих данных:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import javax.persistence.*;
@Entity
@Table(name = "shared_data")
public class SharedData {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String key;
private String value;
// Getters, setters...
}
@Repository
public interface SharedDataRepository extends JpaRepository<SharedData, Long> {
SharedData findByKey(String key);
}
@Service
public class PersistentSharedDataService {
private final SharedDataRepository repository;
public PersistentSharedDataService(SharedDataRepository repository) {
this.repository = repository;
}
public void setData(String key, String value) {
SharedData data = repository.findByKey(key);
if (data == null) {
data = new SharedData();
data.setKey(key);
}
data.setValue(value);
repository.save(data);
}
public String getData(String key) {
SharedData data = repository.findByKey(key);
return data != null ? data.getValue() : null;
}
}
6. Cache (Redis, Caffeine)
Для высокопроизводительного кэширования:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class CachedSharedDataService {
@Cacheable(value = "shared-data", key = "#key")
public String getData(String key) {
// Если в кэше нет — запрос к БД или вычисление
return fetchDataFromDatabase(key);
}
@CachePut(value = "shared-data", key = "#key")
public String setData(String key, String value) {
// Обновляем в БД
saveToDatabase(key, value);
return value;
}
private String fetchDataFromDatabase(String key) {
// Реализация...
return "data";
}
private void saveToDatabase(String key, String value) {
// Реализация...
}
}
// В application.yml
// spring:
// cache:
// type: caffeine
// caffeine:
// spec: maximumSize=500,expireAfterWrite=10m
7. Полный пример: REST контроллер с общими данными
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.*;
@Component
public class SessionManager {
private final ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>();
public void createSession(String sessionId, String userId) {
sessions.put(sessionId, new UserSession(userId, LocalDateTime.now()));
}
public UserSession getSession(String sessionId) {
return sessions.get(sessionId);
}
public void invalidateSession(String sessionId) {
sessions.remove(sessionId);
}
public int getActiveSessionCount() {
return sessions.size();
}
}
@RestController
@RequestMapping("/api/sessions")
public class SessionController {
private final SessionManager sessionManager;
public SessionController(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@PostMapping
public void createSession(@RequestParam String sessionId, @RequestParam String userId) {
sessionManager.createSession(sessionId, userId);
}
@GetMapping("/{sessionId}")
public UserSession getSession(@PathVariable String sessionId) {
return sessionManager.getSession(sessionId);
}
@DeleteMapping("/{sessionId}")
public void invalidateSession(@PathVariable String sessionId) {
sessionManager.invalidateSession(sessionId);
}
@GetMapping("/count")
public int getActiveCount() {
return sessionManager.getActiveSessionCount();
}
}
public class UserSession {
private final String userId;
private final LocalDateTime createdAt;
public UserSession(String userId, LocalDateTime createdAt) {
this.userId = userId;
this.createdAt = createdAt;
}
// Getters...
}
Сравнение подходов
| Подход | Использование | Потокобезопасность | Масштабируемость |
|---|---|---|---|
| Singleton Bean | Простые случаи | Требует синхронизации | Один сервер |
| ConcurrentHashMap | В памяти, читаемо-тяжелое | Встроенная | Один сервер |
| ReadWriteLock | Много читателей | Встроенная | Один сервер |
| Database | Персистентность | Встроенная | Масштабируемо |
| Redis | Распределённое кэширование | Встроенная | Масштабируемо |
| Session | Веб-сессии | Встроенная | Масштабируемо |
Рекомендации
- Используй ConcurrentHashMap для простых случаев
- Предпочитай Database для критичных данных
- Используй Redis для распределённого кэша
- Избегай synchronized — используй concurrent классы
- Тестируй многопоточность с помощью JUnit + Concurrency тестов
Потокобезопасность в тестах
import org.junit.jupiter.api.Test;
import java.util.concurrent.*;
import static org.junit.jupiter.api.Assertions.*;
public class ThreadSafetyTest {
@Test
public void testConcurrentAccess() throws InterruptedException {
SharedDataService service = new SharedDataService();
ExecutorService executor = Executors.newFixedThreadPool(10);
int iterations = 1000;
for (int i = 0; i < iterations; i++) {
final int value = i;
executor.submit(() -> service.incrementCounter());
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
assertEquals(iterations, service.getCounter());
}
}
Вывод: Используй ConcurrentHashMap + Singleton Bean для простого случая, Database для персистентности, и Redis для распределённых систем.