Interview questions

+1. JVM memory areas. Thread safe memory areas, Garbage collector working principle, Java reference types

+2. SOLID
+3. Collection API: How does HashMap work under the hood, hash collision, Concurrent HashMap
+4. Equals and HashCode contract
-5. Thread, Volatile vs Synchronized, atomic variables(CAS-compare and swap). How to prevent deadlock. Deadlock vs livelock. Reentrant lock.
+6. Error vs Exception
+7. Immutable class(how to make object properties immutable, immutable collections)
8. DB index type(clustered, non clustered, composite)
9. DB normalization and denormalization
10. RDBMS vs non RDBMS
+11. ACID vs BASE. Lock mechanism
12. Hash eviction policy(TTL, LRU, FIFO, LFU, MRU, random replacement, ARC). Problem: we have 2GB memory, how to store 15GB data in this cache
+13. entity lifecycle
+14. dirty check, flush
15. cascade types
+16. n+1 problem (entity graph, join fetch)
+17. lazy initialization exception
18. idempotency, safe in rest methods
19. IOC, DI, Proxy
20. AOP(cross cutting concerns)
21. communication types(REST, gRPC, long polling, short polling, web hook, web socket)
22. kafka vs rabbit mq (pull and push base architecture)
23. leader - follower pattern (leader election pattern)
24. Distributed transaction management - SAGA, two phase and three phase commit
25. Transactional outbox pattern (example: CDC)
26. CQRS, event sourcing(immutable data)
27. monolith to microservice migration patterns
28. Service registry
29. load balancer types
30. blocking and non blocking architecture
31. CAP theorem
32. DB partitioning, DB sharding
33. twelve factor app
34. reflection
35. L1 and L2 caches















1. JVM memory areas: 

JVM runs Java programs and handles memory. Converts .java .class runs on JVM. Handles memory allocation, GC, and execution




Heap area - the larges memory area. 




Stack area.





Metaspace.













Both Stack and Heap are created by JVM and stored in RAM. 


https://www.youtube.com/watch?v=vz6vSZRuS2M&ab_channel=Concept%26%26Coding-byShrayansh



                                                                JVM

There are 2 type of memory: Stack and Heap
Both Stack and Heap are created by JVM and stored in RAM.












Garbage Collctor:

Garbage collection (GC) in Java automatically manages memory by reclaiming heap space occupied by objects that are no longer reachable or referenced by the program. Here's how it works:

Core Principle: Reachability

GC identifies objects that are unreachable from any active part of the program:

  • Objects referenced by local variables on the stack
  • Objects referenced by static variables
  • Objects referenced by other reachable objects

If an object can't be reached through this chain of references, it's eligible for garbage collection.

Generational Garbage Collection

Most JVMs use a generational approach based on the observation that most objects die young:

Young Generation

  • Eden Space: Where new objects are allocated
  • Survivor Spaces (S0, S1): Objects that survive their first GC cycle
  • Uses copying algorithm: Live objects are copied to survivor space, dead objects are removed

Old Generation (Tenured)

  • Long-lived objects that survived multiple GC cycles in young generation
  • Uses mark-and-sweep or mark-sweep-compact algorithms
  • More expensive to collect but happens less frequently

GC Process Steps

  1. Mark Phase: Starting from GC roots (stack variables, static variables), mark all reachable objects
  2. Sweep Phase: Deallocate memory occupied by unmarked (unreachable) objects
  3. Compact Phase (optional): Defragment memory by moving live objects together

Types of GC Events

// Minor GC - cleans young generation (frequent, fast)
// Major GC - cleans old generation (less frequent, slower)  
// Full GC - cleans entire heap (rare, slowest)

Memory Lifecycle Example

public void example() {
    String str = new String("Hello"); // Object created in Eden
    str = null; // Object becomes unreachable
    // Object is now eligible for GC
    System.gc(); // Suggests GC (not guaranteed to run immediately)
}

The beauty of GC is that it happens automatically in the background, freeing developers from manual memory management while preventing memory leaks and ensuring efficient memory usage.




                                                                    Reference types in Java

Java has several types of references that determine how objects interact with garbage collection. Here are the four main reference types:

1. Strong References (Default)

  • Standard object references - what you use in everyday programming
  • Objects with strong references are never garbage collected
  • GC only collects objects when no strong references exist
String str = new String("Hello"); // Strong reference
List<String> list = new ArrayList<>(); // Strong reference
// These objects won't be GC'd while references exist

2. Weak References

  • Objects can be garbage collected even if weak references exist
  • Useful for caches where you want objects to be collected when memory is needed
  • Must check if object still exists before using
import java.lang.ref.WeakReference;

String str = new String("Hello");
WeakReference<String> weakRef = new WeakReference<>(str);
str = null; // Remove strong reference

// Object may be GC'd now
String retrieved = weakRef.get(); // May return null if GC'd
if (retrieved != null) {
    System.out.println(retrieved);
}

3. Soft References

  • Similar to weak references but more lenient
  • GC will try to keep soft-referenced objects longer
  • Only collected when memory pressure is high
  • Ideal for memory-sensitive caches
import java.lang.ref.SoftReference;

SoftReference<LargeObject> softRef = new SoftReference<>(new LargeObject());
// Object will be kept until JVM really needs memory

4. Phantom References

  • Most specialized reference type
  • Object is already eligible for GC but reference allows cleanup actions
  • Always returns null when calling get()
  • Used with ReferenceQueue for cleanup operations
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

ReferenceQueue<LargeObject> queue = new ReferenceQueue<>();
PhantomReference<LargeObject> phantomRef = 
    new PhantomReference<>(obj, queue);
// Used for cleanup after object is finalized

Key Differences Summary

Reference Type GC Behavior Use Case get() Returns Object?
Strong Never collected while reference exists Normal programming Yes
Soft Collected only under memory pressure Memory-sensitive caches Yes (until GC'd)
Weak Can be collected anytime Simple caches, avoiding memory leaks Yes (until GC'd)
Phantom Object already being finalized Cleanup operations No (always null)

Practical Example - Cache Implementation

// WeakHashMap uses weak references for keys
Map<String, Object> cache = new WeakHashMap<>();
cache.put("key", new ExpensiveObject());
// Entries automatically removed when keys are GC'd

The main benefit is preventing memory leaks while still allowing objects to be collected when memory is needed, giving you fine-grained control over object lifecycle and memory management.


* Difference between soft and weak reference is: soft reference is garbage collected only when it is urgent, that means there is no more space and you have to create a space.


Great question! These reference types solve real-world problems that strong references alone can't handle effectively. Let me explain the practical "why" behind each:

Problem 1: Memory Leaks in Caches

Without weak/soft references:

// BAD: This cache will never release objects
Map<String, ExpensiveObject> cache = new HashMap<>();
cache.put("user123", new ExpensiveObject());
// Even if user123 is never needed again, object stays in memory forever!

With weak references:

// GOOD: Objects can be GC'd when not used elsewhere
Map<String, ExpensiveObject> cache = new WeakHashMap<>();
// Entries automatically removed when no other code references the key

Problem 2: Observer Pattern Memory Leaks

Classic memory leak scenario:

public class EventPublisher {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener); // Strong reference!
        // If listener is never removed, it can never be GC'd
    }
}

With weak references:

private List<WeakReference<EventListener>> listeners = new ArrayList<>();
// Listeners can be GC'd even if not explicitly removed

Problem 3: Image/Resource Caching

// Soft references for image cache
Map<String, SoftReference<BufferedImage>> imageCache = new HashMap<>();

public BufferedImage getImage(String path) {
    SoftReference<BufferedImage> ref = imageCache.get(path);
    BufferedImage image = (ref != null) ? ref.get() : null;
    
    if (image == null) {
        image = loadImageFromDisk(path); // Expensive operation
        imageCache.put(path, new SoftReference<>(image));
    }
    return image;
}
// Images stay cached until memory is really needed

Problem 4: Resource Cleanup

Phantom references for cleanup:

// Ensure native resources are cleaned up
public class NativeResource {
    private long nativePointer;
    
    static {
        // Register phantom reference to clean up native memory
        // when Java object is GC'd but before it's fully removed
    }
}

Real-World Benefits

Memory efficiency: Applications can use memory more intelligently

  • Keep frequently used objects
  • Release objects under memory pressure
  • Prevent memory leaks from forgotten references

Better user experience:

  • Apps don't crash from OutOfMemoryError
  • Responsive performance even with limited memory
  • Automatic cleanup without manual intervention

Framework/library design:

  • GUI frameworks use weak references for event listeners
  • Caching libraries use soft references for smart memory management
  • ORM frameworks use weak references to avoid holding onto unused entities

The key insight is that sometimes you want to say: "I'd like to keep this object if possible, but it's okay to throw it away if you need the memory." Strong references can't express this nuanced relationship, but weak and soft references can.




Here are the real-world places where these reference types are actually used:

Weak References - Most Common

1. WeakHashMap (Java Collections)

// Keys can be GC'd even if still in map
WeakHashMap<SomeClass, String> map = new WeakHashMap<>();

2. Event Listeners (Swing, JavaFX, Android)

// GUI frameworks to prevent memory leaks
public class EventManager {
    private List<WeakReference<ActionListener>> listeners = new ArrayList<>();
    // Listeners can be GC'd without explicit removal
}

3. ThreadLocal (Java internals)

// ThreadLocalMap uses weak references to ThreadLocal keys
public class ThreadLocal<T> {
    // Prevents memory leaks when ThreadLocal instances are GC'd
}

4. Hibernate ORM

// Session management to avoid holding onto entities unnecessarily
// Weak references in internal caches

Soft References - Memory-Sensitive Caches

1. Image Caches (Desktop applications)

// Photo editing software, IDEs with image resources
private Map<String, SoftReference<BufferedImage>> imageCache;

2. Google Guava Cache

Cache<String, Object> cache = CacheBuilder.newBuilder()
    .softValues()  // Values kept until memory pressure
    .build();

3. Android Bitmap Caching

// Android apps for image caching
private LruCache<String, SoftReference<Bitmap>> bitmapCache;

4. Compiler Caches (Eclipse JDT, IntelliJ)

// Parsed AST trees, compiled class information
// Keep in memory if possible, release under pressure

Phantom References - Cleanup Operations

1. DirectByteBuffer (Java NIO)

// Cleanup off-heap native memory
public class DirectByteBuffer extends MappedByteBuffer {
    // Uses phantom references + Cleaner for native memory cleanup
}

2. FileInputStream/FileOutputStream

// Ensures file handles are closed even if close() wasn't called
// Java 9+ uses Cleaner API (evolved from phantom references)

3. JDBC Connection Pools

// HikariCP, Apache DBCP use phantom references
// To detect leaked connections and force cleanup

4. Memory Mapped Files

// NIO memory-mapped files use phantom references
// To unmap native memory when Java objects are GC'd

Framework/Library Examples

Spring Framework:

// Bean caches, proxy caches use weak references
private static final Map<Class<?>, Method[]> declaredMethodsCache = 
    new ConcurrentReferenceHashMap<>(256, ConcurrentReferenceHashMap.ReferenceType.WEAK);

Apache Commons:

// WeakHashMap usage in various utilities

Android Development:

// Handler with weak reference to Activity (prevents memory leaks)
private static class MyHandler extends Handler {
    private final WeakReference<MainActivity> activityRef;
}

JVM Internals

Class Loading:

  • Weak references for class metadata
  • Soft references for reflection caches

String Interning:

  • Weak references in string pools (older JVM versions)

Method Compilation:

  • JIT compiler uses soft references for compiled code caches

Key Takeaway

These reference types are mostly used by frameworks and JVM internals rather than everyday application code. As an application developer, you'll mainly encounter them when:

  • Using libraries that implement them (Guava Cache, Spring)
  • Dealing with Android memory management
  • Creating your own caching solutions
  • Working with native resources that need cleanup

Most day-to-day programming uses strong references, but understanding these helps you recognize when libraries are preventing memory leaks or managing resources efficiently.












Minor GC - it works in young generation and is fast and happen very periodically. 
When threshold is passed then the objects will be promoted to old generation.


Major GC - it works on old generation.

Works by Mark and Sweep algorithm. Mark and Sweep and Compaction


There are several versions of GC:
1) Serial GC: only 1 thread is used. It works on young generation. GC is very expensive. When GC work starts all application threads will be paused. 
2) Parallel GC: Default GC in java 8
3) Concurrent Mark and Sweep (CMS). While application threads are working concurrently GC threads are working. If segments appear(free spaces) no memory compactions happens. 
4) G1 GC

Metaspace - non heap memory



2. SOLID

Отлично! Давайте разберем SOLID принципы с примерами на Java.

S - Single Responsibility Principle (SRP)

Принцип: Класс должен иметь только одну причину для изменения.

Плохо:

public class User {
    private String name;
    private String email;
    
    // Много ответственностей в одном классе
    public void save() { /* сохранение в БД */ }
    public void sendEmail() { /* отправка email */ }
    public String generateReport() { /* генерация отчета */ }
    public boolean validateEmail() { /* валидация */ }
}

Хорошо:

public class User {
    private String name;
    private String email;
    // Только данные пользователя
}

public class UserRepository {
    public void save(User user) { /* только сохранение */ }
}

public class EmailService {
    public void sendEmail(User user, String message) { /* только email */ }
}

public class UserReportGenerator {
    public String generateReport(User user) { /* только отчеты */ }
}

O - Open/Closed Principle (OCP)

Принцип: Классы открыты для расширения, закрыты для модификации.

Плохо:

public class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle rect = (Rectangle) shape;
            return rect.width * rect.height;
        } else if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * circle.radius * circle.radius;
        }
        // Нужно модифицировать класс для каждой новой фигуры!
        return 0;
    }
}

Хорошо:

public abstract class Shape {
    public abstract double calculateArea();
}

public class Rectangle extends Shape {
    private double width, height;
    
    @Override
    public double calculateArea() {
        return width * height;
    }
}

public class Circle extends Shape {
    private double radius;
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class AreaCalculator {
    public double calculateArea(Shape shape) {
        return shape.calculateArea(); // Не нужно изменять!
    }
}

L - Liskov Substitution Principle (LSP)

Принцип: Объекты суперкласса должны заменяться объектами подкласса без нарушения функциональности.

Плохо:

public class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
        // Нарушает LSP - нельзя заменить Bird на Penguin
    }
}

Хорошо:

public abstract class Bird {
    public abstract void move();
}

public class FlyingBird extends Bird {
    @Override
    public void move() {
        fly();
    }
    
    protected void fly() {
        System.out.println("Flying...");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        swim();
    }
    
    private void swim() {
        System.out.println("Swimming...");
    }
}

I - Interface Segregation Principle (ISP)

Принцип: Клиенты не должны зависеть от интерфейсов, которые они не используют.

Плохо:

public interface Worker {
    void work();
    void eat();    // Не все работники едят!
    void sleep();  // Не все работники спят!
}

public class Robot implements Worker {
    @Override
    public void work() { /* работает */ }
    
    @Override
    public void eat() {
        throw new UnsupportedOperationException(); // Робот не ест!
    }
    
    @Override
    public void sleep() {
        throw new UnsupportedOperationException(); // Робот не спит!
    }
}

Хорошо:

public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public class Human implements Workable, Eatable, Sleepable {
    @Override
    public void work() { /* работает */ }
    @Override
    public void eat() { /* ест */ }
    @Override
    public void sleep() { /* спит */ }
}

public class Robot implements Workable {
    @Override
    public void work() { /* только работает */ }
}

D - Dependency Inversion Principle (DIP)

Принцип: Зависеть от абстракций, а не от конкретных реализаций.

Плохо:

public class OrderService {
    private MySQLDatabase database; // Зависимость от конкретной реализации
    
    public void saveOrder(Order order) {
        database.save(order); // Привязан к MySQL
    }
}

Хорошо:

public interface Database {
    void save(Order order);
}

public class MySQLDatabase implements Database {
    @Override
    public void save(Order order) { /* MySQL implementation */ }
}

public class PostgreSQLDatabase implements Database {
    @Override
    public void save(Order order) { /* PostgreSQL implementation */ }
}

public class OrderService {
    private Database database; // Зависимость от абстракции
    
    public OrderService(Database database) {
        this.database = database; // Dependency Injection
    }
    
    public void saveOrder(Order order) {
        database.save(order); // Работает с любой реализацией
    }
}

Практический пример - все принципы вместе:

// SRP - каждый класс имеет одну ответственность
public interface PaymentProcessor {
    void processPayment(double amount);
}

// OCP - можем добавлять новые способы оплаты без изменения существующих
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) { /* обработка карты */ }
}

public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) { /* PayPal обработка */ }
}

// DIP - зависим от интерфейса, не от конкретной реализации
public class OrderService {
    private PaymentProcessor paymentProcessor;
    
    public OrderService(PaymentProcessor processor) { // DI
        this.paymentProcessor = processor;
    }
    
    public void processOrder(Order order) {
        // LSP - любая реализация PaymentProcessor работает корректно
        paymentProcessor.processPayment(order.getTotal());
    }
}

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




3. Equals and HashCode contract

Отлично! Contract между equals() и hashCode() - это одна из важнейших тем в Java.

Основные правила Contract

1. Если объекты равны по equals(), их hashCode() должны быть одинаковыми

// ПРАВИЛО: если a.equals(b) == true, то a.hashCode() == b.hashCode()

2. Если hashCode() разные, объекты точно НЕ равны

// Если a.hashCode() != b.hashCode(), то a.equals(b) должно быть false

3. Если hashCode() одинаковые, объекты могут быть как равными, так и НЕ равными

// a.hashCode() == b.hashCode() НЕ гарантирует что a.equals(b) == true
// Это collision - нормальная ситуация

Что происходит при нарушении Contract?

Пример нарушения:

public class BadPerson {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        BadPerson person = (BadPerson) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    // НЕ ПЕРЕОПРЕДЕЛЯЕМ hashCode() - НАРУШЕНИЕ CONTRACT!
    // Используется hashCode() от Object (адрес в памяти)
}

Проблемы:

BadPerson p1 = new BadPerson("John", 25);
BadPerson p2 = new BadPerson("John", 25);

System.out.println(p1.equals(p2)); // true - объекты равны
System.out.println(p1.hashCode() == p2.hashCode()); // false! - разные hashCode

// КАТАСТРОФА в HashMap/HashSet:
Set<BadPerson> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // FALSE! Хотя p1.equals(p2) == true
// HashMap не может найти объект, потому что ищет в wrong bucket!

Правильная реализация

public class Person {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Используем те же поля, что и в equals!
    }
}

Теперь все работает:

Person p1 = new Person("John", 25);
Person p2 = new Person("John", 25);

System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true

Set<Person> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // TRUE! Работает корректно

Как работает HashMap внутри

// Упрощенная логика HashMap.get()
public V get(Object key) {
    int hash = key.hashCode(); // 1. Получаем hashCode
    int bucket = hash % buckets.length; // 2. Вычисляем bucket
    
    // 3. Ищем в bucket-е используя equals()
    for (Entry entry : buckets[bucket]) {
        if (entry.key.equals(key)) { // Здесь используется equals()!
            return entry.value;
        }
    }
    return null;
}

Что происходит при нарушении:

  • Разные hashCode() → ищем в wrong bucket → объект не найден
  • Одинаковые hashCode(), но неправильный equals() → wrong comparison

Детальные правила equals()

public class ProperEquals {
    private String name;
    private Integer id;
    
    @Override
    public boolean equals(Object obj) {
        // 1. Reflexive: x.equals(x) должно быть true
        if (this == obj) return true;
        
        // 2. Null check
        if (obj == null) return false;
        
        // 3. Class check
        if (getClass() != obj.getClass()) return false;
        
        // 4. Field comparison
        ProperEquals other = (ProperEquals) obj;
        return Objects.equals(name, other.name) && 
               Objects.equals(id, other.id);
    }
    
    @Override
    public int hashCode() {
        // Используем ТЕ ЖЕ поля, что и в equals!
        return Objects.hash(name, id);
    }
}

Правила для equals()

  1. Reflexive: x.equals(x) всегда true
  2. Symmetric: если x.equals(y), то y.equals(x)
  3. Transitive: если x.equals(y) и y.equals(z), то x.equals(z)
  4. Consistent: множественные вызовы дают одинаковый результат
  5. Null: x.equals(null) всегда false

Правила для hashCode()

  1. Consistent: должен возвращать одинаковое значение при множественных вызовах
  2. Equal objects: если equals() возвращает true, hashCode() должны быть равны
  3. Performance: желательно равномерное распределение для производительности

Практические советы

Используйте IDE для генерации:

// IntelliJ IDEA: Alt+Insert → equals() and hashCode()
// Eclipse: Source → Generate hashCode() and equals()

Используйте утилиты:

// Java 7+
Objects.equals(field1, other.field1)
Objects.hash(field1, field2, field3)

// Apache Commons Lang
EqualsBuilder.reflectionEquals(this, obj)
HashCodeBuilder.reflectionHashCode(this)

Lombok для ленивых:

@EqualsAndHashCode
public class Person {
    private String name;
    private int age;
    // equals() и hashCode() генерируются автоматически
}

Главное правило: Всегда переопределяйте equals() и hashCode() вместе, используя одинаковые поля в обоих методах!



                                                            Internal working of HashMap




                                                        Fail Fast and Fail Safe
















Отличный вопрос! Fail-fast и fail-safe - это две стратегии поведения при обнаружении ошибок или проблем.

Fail-Fast - "Упади быстро"

Принцип: При обнаружении проблемы немедленно выбросить исключение и остановить выполнение.

Примеры Fail-Fast в Java:

1. ArrayList при concurrent modification:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for (String item : list) {
    if ("B".equals(item)) {
        list.remove(item); // ConcurrentModificationException!
    }
}
// Fail-fast: сразу падает, не пытается продолжить

2. Iterator с modCount:

List<String> list = new ArrayList<>();
Iterator<String> iter = list.iterator();
list.add("new item"); // Изменяем список
iter.next(); // ConcurrentModificationException!

3. Null checks:

public void processUser(User user) {
    Objects.requireNonNull(user, "User cannot be null"); // Fail-fast
    // Лучше упасть сразу, чем получить NullPointerException позже
}

Fail-Safe - "Безопасное продолжение"

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

Примеры Fail-Safe в Java:

1. ConcurrentHashMap:

Map<String, String> map = new ConcurrentHashMap<>();
map.put("A", "1");
map.put("B", "2");

for (Map.Entry<String, String> entry : map.entrySet()) {
    map.put("C", "3"); // НЕ выбрасывает исключение!
    // Может увидеть или не увидеть новый элемент, но не падает
}

2. CopyOnWriteArrayList:

List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");

for (String item : list) {
    list.add("C"); // Безопасно! Итератор работает со snapshot
}
// Итератор видит состояние на момент создания

3. Optional для избежания null:

// Fail-safe подход с Optional
public Optional<User> findUser(Long id) {
    // Возвращаем Optional вместо null или исключения
    return Optional.ofNullable(database.findById(id));
}

// Использование
findUser(123L)
    .map(User::getName)
    .orElse("Unknown User"); // Безопасно обрабатываем отсутствие

Когда использовать каждый подход?

Fail-Fast подходит когда:

1. Ошибки в разработке:

public void withdraw(double amount) {
    if (amount <= 0) {
        throw new IllegalArgumentException("Amount must be positive"); // Fail-fast
    }
    if (amount > balance) {
        throw new InsufficientFundsException(); // Fail-fast
    }
}

2. Критические системы:

public void launchRocket() {
    validateAllSystems(); // Лучше не запускать, чем запустить с ошибкой
    launch();
}

Fail-Safe подходит когда:

1. Пользовательские интерфейсы:

public String getUserDisplayName(User user) {
    if (user == null || user.getName() == null) {
        return "Guest User"; // Fail-safe: показываем что-то разумное
    }
    return user.getName();
}

2. Сетевые операации:

public List<Product> getProducts() {
    try {
        return apiClient.fetchProducts();
    } catch (NetworkException e) {
        logger.warn("Failed to fetch products, using cache", e);
        return cachedProducts; // Fail-safe: fallback к кешу
    }
}

3. Конфигурация:

public int getMaxConnections() {
    String value = config.getProperty("max.connections");
    try {
        return Integer.parseInt(value);
    } catch (NumberFormatException e) {
        return 10; // Fail-safe: default value
    }
}

Сравнение подходов:

Аспект Fail-Fast Fail-Safe
Философия "Лучше упасть, чем работать неправильно" "Лучше работать частично, чем не работать"
Производительность Может быть медленнее (проверки) Может быть быстрее (меньше проверок)
Отладка Легче найти ошибки Ошибки могут скрываться
Пользовательский опыт Может расстроить пользователя Более плавный опыт
Критичность Хорошо для критических систем Хорошо для UI и сервисов

Практический совет:

Используйте fail-fast для:

  • Валидации входных параметров
  • Обнаружения багов в разработке
  • Критических бизнес-операций

Используйте fail-safe для:

  • Пользовательских интерфейсов
  • Внешних API и сетевых вызовов
  • Конфигурации и настроек
  • Ситуаций где частичная функциональность лучше полного отказа

Золотое правило: Fail-fast для обнаружения проблем, fail-safe для их graceful обработки!




                                                    HashMap, HashTable, ConcurrentHashMap


HashTable - all methods are synchronized. Only one thread are allowed.

ConcurrentHashMap - multiple threads reading and writing at the same time (providing accessing the same key or buckets)










                                                                Error vs Exception


Отличный вопрос для интервью! Различие между Error и Exception - это fundamental тема в Java.

Иерархия Throwable

Throwable
├── Error (системные ошибки)
└── Exception 
    ├── RuntimeException (unchecked)
    └── Checked Exceptions

Error - Системные ошибки

Характеристики:

  • Серьезные системные проблемы
  • НЕ должны перехватываться приложением
  • Обычно приводят к завершению программы
  • Unchecked (не требуют обработки)

Примеры Error:

// OutOfMemoryError
List<String> list = new ArrayList<>();
while(true) {
    list.add(new String(new char[1000000])); // OOME!
}

// StackOverflowError  
public void recursiveMethod() {
    recursiveMethod(); // Бесконечная рекурсия
}

// NoClassDefFoundError
// Класс был при компиляции, но отсутствует при выполнении

// VirtualMachineError
// Проблемы с JVM

Exception - Исключения приложения

Checked Exceptions

Должны быть обработаны или объявлены:

// IOException
public void readFile() throws IOException {
    FileReader file = new FileReader("file.txt"); // Может выбросить IOException
}

// SQLException
public void queryDatabase() throws SQLException {
    Connection conn = DriverManager.getConnection(url);
    // Должен обработать или пробросить дальше
}

// ClassNotFoundException
public void loadClass() throws ClassNotFoundException {
    Class.forName("com.example.MyClass");
}

Unchecked Exceptions (RuntimeException)

Не требуют обязательной обработки:

// NullPointerException
String str = null;
int length = str.length(); // NPE!

// IndexOutOfBoundsException
List<String> list = new ArrayList<>();
String item = list.get(0); // IOOBE!

// IllegalArgumentException
public void withdraw(double amount) {
    if (amount < 0) {
        throw new IllegalArgumentException("Amount cannot be negative");
    }
}

Типичные вопросы на интервью

1. "В чем разница между Error и Exception?"

Правильный ответ:

// Error - системные проблемы, НЕ перехватываем
try {
    // код
} catch (OutOfMemoryError e) { // ❌ ПЛОХАЯ практика!
    // Не пытайтесь "исправить" системные ошибки
}

// Exception - проблемы приложения, можно обработать
try {
    // код
} catch (FileNotFoundException e) { // ✅ ХОРОШО
    // Можем обработать отсутствие файла
}

2. "Можно ли перехватить Error?"

Технически можно, но НЕ нужно:

try {
    recursiveMethod();
} catch (StackOverflowError e) {
    // Технически скомпилируется, но бессмысленно
    // Стек уже поврежден, программа в непредсказуемом состоянии
}

3. "Что такое checked vs unchecked?"

// Checked - ДОЛЖНЫ обработать
public void method1() {
    try {
        Thread.sleep(1000); // InterruptedException - checked
    } catch (InterruptedException e) {
        // Обязательно обработать
    }
}

// Unchecked - можем НЕ обрабатывать
public void method2() {
    int result = 10 / 0; // ArithmeticException - unchecked
    // Компилятор не заставляет обрабатывать
}

4. "Когда использовать checked vs unchecked?"

Checked Exceptions - когда клиент МОЖЕТ обработать:

public User findUser(Long id) throws UserNotFoundException { // Checked
    // Клиент может показать "User not found" или создать нового
}

public void saveToFile(String data) throws IOException { // Checked  
    // Клиент может попробовать другой файл или показать ошибку
}

Unchecked Exceptions - программные ошибки:

public void withdraw(double amount) {
    if (amount < 0) {
        throw new IllegalArgumentException(); // Unchecked - это баг в коде
    }
}

5. "Что будет если не поймать Error?"

public class ErrorDemo {
    public static void main(String[] args) {
        System.out.println("Before error");
        causeStackOverflow();
        System.out.println("After error"); // Никогда не выполнится
    }
    
    static void causeStackOverflow() {
        causeStackOverflow(); // StackOverflowError
    }
}
// Вывод:
// Before error
// Exception in thread "main" java.lang.StackOverflowError
// Программа завершается

Практические примеры для интервью

Scenario 1: "Как обработать OutOfMemoryError?"

Неправильно:

try {
    // код
} catch (OutOfMemoryError e) {
    // Попытаться "почистить" память - бессмысленно
}

Правильно:

// Предотвращение: мониторинг памяти
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
long usedMemory = memoryBean.getHeapMemoryUsage().getUsed();
// Логировать и настроить алерты

Scenario 2: "Создание custom exception"

// Checked exception для бизнес-логики
public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

// Unchecked exception для программных ошибок
public class InvalidConfigurationException extends RuntimeException {
    public InvalidConfigurationException(String message) {
        super(message);
    }
}

Типичные ошибки кандидатов

❌ Неправильные ответы:

  • "Error и Exception - это одно и то же"
  • "Error можно и нужно перехватывать"
  • "Все исключения должны быть checked"
  • "Runtime exceptions нельзя перехватывать"

✅ Правильные ответы:

  • "Error для системных проблем, Exception для логики приложения"
  • "Error обычно не перехватываем, Exception - обрабатываем"
  • "Checked exceptions принуждают к обработке, unchecked - нет"
  • "Используем checked когда клиент может разумно обработать ошибку"

Подводные камни на собеседовании

Вопрос-ловушка: "Можно ли extend Error?"

// Технически можно, но НЕ нужно
public class MyError extends Error { // Компилируется, но плохая идея
    // Ваши Error должны быть системными проблемами
}

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

Золотое правило для интервью: Error = системные проблемы (не трогаем), Exception = проблемы приложения (обрабатываем разумно).




                                                                    Immutable class


Отлично! Давайте основательно разберем Immutable классы - это очень важная и популярная тема на собеседованиях.

Что такое Immutable класс?

Immutable класс - это класс, объекты которого нельзя изменить после создания. Все поля остаются неизменными на протяжении всей жизни объекта.

Правила создания Immutable класса

1. Все поля должны быть final

public final class Person {
    private final String name;
    private final int age;
    private final List<String> hobbies;
    
    // constructor...
}

2. Класс должен быть final (нельзя наследовать)

public final class Person { // final class!
    // Предотвращает создание mutable подклассов
}

3. Нет setter методов

public final class Person {
    private final String name;
    
    // ❌ НЕТ setter'ов!
    // public void setName(String name) { ... }
    
    // ✅ Только getter'ы
    public String getName() {
        return name;
    }
}

4. Все поля приватные

public final class Person {
    private final String name; // private!
    private final int age;     // private!
}

5. Инициализация только через конструктор

public final class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Простой пример Immutable класса

public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final String email;
    
    public ImmutablePerson(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getEmail() { return email; }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}";
    }
}

Проблема с mutable полями

❌ Неправильно - нарушение immutability:

public final class BadImmutableClass {
    private final List<String> hobbies;
    
    public BadImmutableClass(List<String> hobbies) {
        this.hobbies = hobbies; // ПРОБЛЕМА!
    }
    
    public List<String> getHobbies() {
        return hobbies; // ПРОБЛЕМА!
    }
}

// Использование:
List<String> hobbies = new ArrayList<>();
hobbies.add("Reading");

BadImmutableClass person = new BadImmutableClass(hobbies);
hobbies.add("Swimming"); // Изменили исходный список!

person.getHobbies().add("Gaming"); // Изменили через getter!
// Объект больше не immutable!

✅ Правильно - defensive copying:

public final class GoodImmutableClass {
    private final List<String> hobbies;
    
    public GoodImmutableClass(List<String> hobbies) {
        // Defensive copy в конструкторе
        this.hobbies = new ArrayList<>(hobbies);
    }
    
    public List<String> getHobbies() {
        // Defensive copy в getter'е
        return new ArrayList<>(hobbies);
    }
}

Immutable Collections - правильный подход

Collections.unmodifiableList() - shallow immutability

public final class PersonWithHobbies {
    private final List<String> hobbies;
    
    public PersonWithHobbies(List<String> hobbies) {
        this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
    }
    
    public List<String> getHobbies() {
        return hobbies; // Уже unmodifiable
    }
}

Google Guava - настоящие immutable collections

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableMap;

public final class PersonWithGuava {
    private final ImmutableList<String> hobbies;
    private final ImmutableSet<String> skills;
    private final ImmutableMap<String, String> contacts;
    
    public PersonWithGuava(List<String> hobbies, Set<String> skills, Map<String, String> contacts) {
        this.hobbies = ImmutableList.copyOf(hobbies);
        this.skills = ImmutableSet.copyOf(skills);
        this.contacts = ImmutableMap.copyOf(contacts);
    }
    
    public ImmutableList<String> getHobbies() { return hobbies; }
    public ImmutableSet<String> getSkills() { return skills; }
    public ImmutableMap<String, String> getContacts() { return contacts; }
}

Java 9+ - List.of(), Set.of(), Map.of()

public final class ModernImmutableClass {
    private final List<String> hobbies;
    private final Set<String> skills;
    private final Map<String, String> contacts;
    
    public ModernImmutableClass(List<String> hobbies, Set<String> skills, Map<String, String> contacts) {
        this.hobbies = List.copyOf(hobbies);     // Java 10+
        this.skills = Set.copyOf(skills);        // Java 10+
        this.contacts = Map.copyOf(contacts);    // Java 10+
    }
    
    // Или создание через factory методы
    public static ModernImmutableClass create() {
        return new ModernImmutableClass(
            List.of("reading", "swimming"),      // Java 9+
            Set.of("java", "python"),           // Java 9+
            Map.of("email", "test@test.com")    // Java 9+
        );
    }
}

Сложный пример - nested objects

public final class ImmutableAddress {
    private final String street;
    private final String city;
    private final String country;
    
    public ImmutableAddress(String street, String city, String country) {
        this.street = street;
        this.city = city;
        this.country = country;
    }
    
    // getters...
    public String getStreet() { return street; }
    public String getCity() { return city; }
    public String getCountry() { return country; }
}

public final class ImmutableEmployee {
    private final String name;
    private final ImmutableAddress address;
    private final List<String> projects;
    private final LocalDate hireDate;
    
    public ImmutableEmployee(String name, ImmutableAddress address, 
                           List<String> projects, LocalDate hireDate) {
        this.name = name;
        this.address = address; // ImmutableAddress уже immutable
        this.projects = List.copyOf(projects); // Deep copy для коллекции
        this.hireDate = hireDate; // LocalDate immutable
    }
    
    // getters...
    public String getName() { return name; }
    public ImmutableAddress getAddress() { return address; }
    public List<String> getProjects() { return projects; }
    public LocalDate getHireDate() { return hireDate; }
}

Builder Pattern для immutable классов

public final class ImmutableUser {
    private final String firstName;
    private final String lastName;
    private final String email;
    private final int age;
    private final List<String> roles;
    
    private ImmutableUser(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.email = builder.email;
        this.age = builder.age;
        this.roles = List.copyOf(builder.roles);
    }
    
    // getters...
    
    public static class Builder {
        private String firstName;
        private String lastName;
        private String email;
        private int age;
        private List<String> roles = new ArrayList<>();
        
        public Builder setFirstName(String firstName) {
            this.firstName = firstName;
            return this;
        }
        
        public Builder setLastName(String lastName) {
            this.lastName = lastName;
            return this;
        }
        
        public Builder setEmail(String email) {
            this.email = email;
            return this;
        }
        
        public Builder setAge(int age) {
            this.age = age;
            return this;
        }
        
        public Builder addRole(String role) {
            this.roles.add(role);
            return this;
        }
        
        public ImmutableUser build() {
            return new ImmutableUser(this);
        }
    }
}

// Использование:
ImmutableUser user = new ImmutableUser.Builder()
    .setFirstName("John")
    .setLastName("Doe")
    .setEmail("john@example.com")
    .setAge(30)
    .addRole("ADMIN")
    .addRole("USER")
    .build();

Records в Java 14+ (автоматически immutable)

// Java 14+ - автоматически immutable!
public record PersonRecord(String name, int age, List<String> hobbies) {
    
    // Compact constructor для validation и defensive copying
    public PersonRecord {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be null or empty");
        }
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
        // Defensive copy
        hobbies = List.copyOf(hobbies);
    }
    
    // Accessor methods генерируются автоматически
    // equals(), hashCode(), toString() тоже автоматически
}

Проблемы с Immutability

1. Проблема с Date (mutable!)

// ❌ ПЛОХО - Date mutable!
public final class BadEvent {
    private final Date eventDate;
    
    public BadEvent(Date eventDate) {
        this.eventDate = eventDate; // ПРОБЛЕМА!
    }
    
    public Date getEventDate() {
        return eventDate; // ПРОБЛЕМА!
    }
}

// Использование:
Date date = new Date();
BadEvent event = new BadEvent(date);
date.setTime(System.currentTimeMillis()); // Изменили дату в объекте!

event.getEventDate().setTime(0); // Тоже изменили!
// ✅ ХОРОШО - defensive copying или используем immutable типы
public final class GoodEvent {
    private final LocalDateTime eventDate; // LocalDateTime immutable
    
    public GoodEvent(LocalDateTime eventDate) {
        this.eventDate = eventDate;
    }
    
    public LocalDateTime getEventDate() {
        return eventDate; // Безопасно - immutable
    }
    
    // Или если нужно использовать Date:
    private final Date legacyDate;
    
    public GoodEvent(Date legacyDate) {
        this.legacyDate = new Date(legacyDate.getTime()); // Defensive copy
    }
    
    public Date getLegacyDate() {
        return new Date(legacyDate.getTime()); // Defensive copy
    }
}

Преимущества Immutable объектов

1. Thread Safety

// Immutable объекты автоматически thread-safe!
ImmutableUser user = new ImmutableUser.Builder()
    .setName("John")
    .build();

// Можно безопасно передавать между потоками без синхронизации

2. Caching (hashCode)

public final class CachedHashCode {
    private final String name;
    private final int age;
    private final int hashCode; // Кешируем hashCode
    
    public CachedHashCode(String name, int age) {
        this.name = name;
        this.age = age;
        this.hashCode = Objects.hash(name, age); // Вычисляем один раз
    }
    
    @Override
    public int hashCode() {
        return hashCode; // Просто возвращаем закешированное значение
    }
}

3. Безопасность

// Нельзя случайно изменить состояние
// Идеально для value objects, configuration objects

Частые ошибки при создании Immutable классов

1. Забыли сделать класс final

// ❌ ПЛОХО
public class NotFinalClass { // Можно унаследовать и сломать immutability
    private final String name;
    // ...
}

// ✅ ХОРОШО  
public final class FinalClass {
    private final String name;
    // ...
}

2. Забыли defensive copying для mutable полей

// ❌ ПЛОХО
private final List<String> items;
public MyClass(List<String> items) {
    this.items = items; // Ссылка на внешний mutable объект!
}

// ✅ ХОРОШО
private final List<String> items;
public MyClass(List<String> items) {
    this.items = List.copyOf(items); // Defensive copy
}

3. Вернули mutable объект из getter'а

// ❌ ПЛОХО
public List<String> getItems() {
    return items; // Если items mutable, то нарушается immutability
}

// ✅ ХОРОШО
public List<String> getItems() {
    return List.copyOf(items); // Или используйте immutable collections
}

Вопросы на собеседовании

Q: "Как сделать класс immutable?" A: Final class, final fields, no setters, defensive copying, immutable collections.

Q: "Зачем нужны immutable объекты?"
A: Thread safety, caching, безопасность, predictability.

Q: "String immutable. Как работает конкатенация?" A: Создается новый String объект, старый остается неизменным.

Q: "Можно ли изменить final поле?" A: Нельзя переназначить ссылку, но можно изменить содержимое mutable объекта.

Золотое правило: Immutable = final class + final fields + no setters + defensive copying + immutable collections!




                                                                    Multithreading




                                                     Optimistic and Pessimistic locking

Отлично! Это очень важная тема. Объясню простым языком с понятными аналогиями.

Аналогия с библиотекой

Представьте библиотеку с редкой книгой, которую хотят читать/редактировать несколько человек одновременно.

Pessimistic Locking (Пессимистичное блокирование)

Философия: "Лучше заблокировать заранее, чем потом разбираться с конфликтами"

Pessimistic Read Lock (Shared Lock)

// Аналогия: "Я читаю эту книгу, никто не может её изменять, но другие могут читать"

Как работает:

@Transactional
public Account readAccount(Long id) {
    // БЛОКИРУЕМ account для чтения
    Account account = accountRepository.findByIdWithReadLock(id);
    //    ↑
    // База данных говорит: "Окей, ты читаешь account 123. 
    // Другие тоже могут читать, но НИКТО не может изменять до твоего COMMIT"
    
    Thread.sleep(5000); // 5 секунд читаем
    return account;
}

// SQL под капотом:
// SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;

Что происходит с другими потоками:

// Thread 1: Читает account с read lock ✅
Account acc1 = service.readAccount(123L);

// Thread 2: Тоже может читать тот же account ✅  
Account acc2 = service.readAccount(123L);

// Thread 3: Пытается ИЗМЕНИТЬ account ❌ ЖДЁТ!
service.updateAccount(123L, newData); // Блокируется до завершения Thread 1

Pessimistic Write Lock (Exclusive Lock)

// Аналогия: "Я редактирую книгу, НИКТО больше не может её ни читать, ни изменять"

Как работает:

@Transactional
public void updateAccount(Long id, BigDecimal newBalance) {
    // БЛОКИРУЕМ account эксклюзивно
    Account account = accountRepository.findByIdWithWriteLock(id);
    //    ↑
    // База данных: "Окей, account 123 теперь ТВОЙ. 
    // Никто другой не может ни читать, ни изменять до твоего COMMIT"
    
    account.setBalance(newBalance);
    Thread.sleep(3000); // Долго думаем
    accountRepository.save(account);
}

// SQL под капотом:
// SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

Что происходит с другими потоками:

// Thread 1: Обновляет account с write lock
service.updateAccount(123L, new BigDecimal("1000"));

// Thread 2: Хочет прочитать тот же account ❌ ЖДЁТ!
Account acc = service.readAccount(123L); // Заблокирован

// Thread 3: Хочет изменить account ❌ ЖДЁТ!
service.updateAccount(123L, new BigDecimal("2000")); // Заблокирован

// Все ждут пока Thread 1 не сделает COMMIT!

Optimistic Locking (Оптимистичное блокирование)

Философия: "Скорее всего конфликтов не будет, проверим только в конце"

Аналогия: "Возьму копию книги, поработаю с ней, а потом проверю - не изменил ли кто-то оригинал за это время"

Optimistic Read

@Entity
public class Account {
    @Id
    private Long id;
    
    private BigDecimal balance;
    
    @Version  // ЭТО КЛЮЧ к optimistic locking!
    private Long version;
}

Как работает:

@Transactional
public Account optimisticRead(Long id) {
    // НЕ блокируем, просто читаем + запоминаем версию
    Account account = accountRepository.findById(id).get();
    // version = 5 (например)
    
    Thread.sleep(5000); // 5 секунд работаем с данными
    
    // Никто не заблокирован! Все могут читать и изменять параллельно
    return account;
}

// SQL под капотом:
// SELECT id, balance, version FROM accounts WHERE id = 1;
// Никаких LOCK'ов!

Optimistic Write

@Transactional
public void optimisticUpdate(Long id, BigDecimal newBalance) {
    // Шаг 1: Читаем без блокировки
    Account account = accountRepository.findById(id).get();
    // Запомнили: version = 5
    
    // Шаг 2: Работаем с данными (никого не блокируем)
    account.setBalance(newBalance);
    Thread.sleep(3000); // Долго думаем
    
    // Шаг 3: При сохранении - ПРОВЕРЯЕМ версию!
    accountRepository.save(account);
    // Hibernate проверит: version все еще = 5?
}

// SQL при save():
// UPDATE accounts SET balance = ?, version = version + 1 
// WHERE id = ? AND version = 5;
//                    ↑
//              Проверка версии!

Что происходит при конфликте:

// Thread 1: Читает account (version = 5)
Account acc1 = accountRepository.findById(123L).get(); // version = 5

// Thread 2: Тоже читает account (version = 5) 
Account acc2 = accountRepository.findById(123L).get(); // version = 5

// Thread 1: Изменяет и сохраняет первым
acc1.setBalance(new BigDecimal("1000"));
accountRepository.save(acc1); // ✅ Успех! version стала = 6

// Thread 2: Пытается сохранить свои изменения  
acc2.setBalance(new BigDecimal("2000"));
accountRepository.save(acc2); 
// ❌ OptimisticLockException! 
// version уже не 5, а 6 - кто-то изменил данные!

Сравнение подходов

Pessimistic Locking

// ✅ Плюсы:
// - Гарантированно нет конфликтов
// - Данные защищены от изменений
// - Предсказуемое поведение

// ❌ Минусы:  
// - Блокирует других пользователей
// - Может привести к deadlock'ам
// - Снижает производительность
// - Длительные блокировки

@Transactional
public void pessimisticTransfer() {
    Account from = repo.findWithWriteLock(1L); // БЛОК!
    Account to = repo.findWithWriteLock(2L);   // БЛОК!
    
    // Никто не может работать с этими account'ами
    Thread.sleep(10000); // 10 секунд блокировки!
    
    transfer(from, to, amount);
} // Только тут разблокировка

Optimistic Locking

// ✅ Плюсы:
// - Высокая производительность
// - Нет блокировок
// - Нет deadlock'ов
// - Масштабируется хорошо

// ❌ Минусы:
// - Возможны конфликты (retry нужен)
// - Усложняет код обработкой исключений
// - Не подходит для частых изменений

@Transactional
public void optimisticTransfer() {
    Account from = repo.findById(1L).get(); // Не блокируем
    Account to = repo.findById(2L).get();   // Не блокируем
    
    // Все могут параллельно работать с данными!
    Thread.sleep(10000); // Никого не блокируем
    
    try {
        transfer(from, to, amount);
    } catch (OptimisticLockException e) {
        // Retry логика нужна
    }
}

Реальные примеры использования

Пример 1: Банковский перевод (Pessimistic)

@Service
public class BankService {
    
    // Для критических операций с деньгами - pessimistic
    @Transactional
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        // ОБЯЗАТЕЛЬНО блокируем - деньги не шутка!
        Account from = accountRepository.findByIdForUpdate(fromId);
        Account to = accountRepository.findByIdForUpdate(toId);
        
        from.withdraw(amount);
        to.deposit(amount);
        // Другие не могут изменить балансы до завершения перевода
    }
}

// Repository:
@Query("SELECT a FROM Account a WHERE a.id = :id")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Account findByIdForUpdate(@Param("id") Long id);

Пример 2: Редактирование профиля (Optimistic)

@Service  
public class UserProfileService {
    
    // Для некритичных данных - optimistic
    @Transactional
    public void updateProfile(Long userId, UserProfileDto dto) {
        try {
            User user = userRepository.findById(userId).get();
            user.updateProfile(dto);
            userRepository.save(user); // Может выбросить OptimisticLockException
            
        } catch (OptimisticLockException e) {
            // Retry или показать пользователю конфликт
            throw new ProfileUpdateConflictException(
                "Профиль был изменен другим пользователем. Обновите страницу."
            );
        }
    }
}

Пример 3: Инвентарь товара (Mixed подход)

@Service
public class InventoryService {
    
    // Критично для остатков - pessimistic
    @Transactional
    public void reserveItem(Long itemId, int quantity) {
        InventoryItem item = inventoryRepository.findByIdForUpdate(itemId);
        
        if (item.getQuantity() < quantity) {
            throw new InsufficientStockException();
        }
        
        item.reserve(quantity);
        // Блокируем чтобы избежать overselling
    }
    
    // Некритично для описания - optimistic  
    @Transactional
    public void updateDescription(Long itemId, String description) {
        InventoryItem item = inventoryRepository.findById(itemId).get();
        item.setDescription(description);
        inventoryRepository.save(item); // Optimistic
    }
}

Retry паттерн для Optimistic Lock

@Service
public class RetryService {
    
    @Retryable(
        value = {OptimisticLockException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 100)
    )
    @Transactional
    public void updateWithRetry(Long id, String newValue) {
        Entity entity = repository.findById(id).get();
        entity.setValue(newValue);
        repository.save(entity); // Может упасть, тогда retry
    }
    
    // Или ручной retry:
    public void manualRetry(Long id, String newValue) {
        int maxAttempts = 3;
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                updateEntity(id, newValue);
                return; // Успех!
            } catch (OptimisticLockException e) {
                if (attempt == maxAttempts) {
                    throw new RetryExhaustedException("Не удалось обновить после " + maxAttempts + " попыток");
                }
                // Ждем и повторяем
                Thread.sleep(100 * attempt);
            }
        }
    }
}

Deadlock в Pessimistic Lock

// ПЛОХО - может быть deadlock
@Transactional
public void badTransfer() {
    Account from = repo.findWithLock(1L); // Thread 1 блокирует account 1
    Thread.sleep(100);
    Account to = repo.findWithLock(2L);   // Thread 1 ждет account 2
    // Но Thread 2 уже заблокировал account 2 и ждет account 1!
    // DEADLOCK!
}

// ХОРОШО - всегда блокируем в одном порядке
@Transactional  
public void goodTransfer(Long fromId, Long toId) {
    // Сортируем ID
    Long firstId = Math.min(fromId, toId);
    Long secondId = Math.max(fromId, toId);
    
    Account first = repo.findWithLock(firstId);
    Account second = repo.findWithLock(secondId);
    
    // Deadlock невозможен!
}

Когда что использовать?

Pessimistic Lock используйте когда:

  • Критические данные (деньги, остатки товара)
  • Частые конфликты ожидаются
  • Простота логики важнее производительности
  • Короткие транзакции

Optimistic Lock используйте когда:

  • Редкие конфликты ожидаются
  • Высокая производительность критична
  • Много параллельных чтений
  • Некритичные данные (профили, настройки)

Золотое правило: Для денег и критических ресурсов - pessimistic, для всего остального - optimistic!




                                                                        N + 1 problem

Отлично! N+1 проблема - это одна из самых частых проблем производительности в ORM. Разберем детально!

Что такое N+1 проблема?

N+1 проблема - это когда вместо одного SQL запроса выполняется N+1 запросов:

  • 1 запрос для получения списка главных сущностей
  • N запросов для получения связанных данных (где N = количество главных сущностей)

Простой пример проблемы

Модели данных

@Entity
public class User {
    @Id
    private Long id;
    private String name;
    
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // LAZY по умолчанию
    private List<Order> orders;
    
    // getters/setters
}

@Entity  
public class Order {
    @Id
    private Long id;
    private BigDecimal amount;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
    
    // getters/setters
}

Код, который вызывает N+1

@Service
public class UserService {
    
    public void printUsersWithOrders() {
        // 1. Получаем всех пользователей
        List<User> users = userRepository.findAll();
        // SQL: SELECT * FROM users; (1 запрос)
        
        // 2. Для каждого пользователя получаем его заказы
        for (User user : users) {
            System.out.println("User: " + user.getName());
            
            List<Order> orders = user.getOrders(); // Ленивая загрузка!
            // SQL: SELECT * FROM orders WHERE user_id = ?; (N запросов!)
            
            System.out.println("Orders count: " + orders.size());
        }
    }
}

Что происходит в базе данных:

-- 1-й запрос (получаем пользователей):
SELECT * FROM users;
-- Результат: 1000 пользователей

-- 2-й запрос (заказы для user_id = 1):  
SELECT * FROM orders WHERE user_id = 1;

-- 3-й запрос (заказы для user_id = 2):
SELECT * FROM orders WHERE user_id = 2;

-- 4-й запрос (заказы для user_id = 3):
SELECT * FROM orders WHERE user_id = 3;

-- ... и так далее до 1001-го запроса!
-- ИТОГО: 1001 запрос вместо одного!

Визуализация проблемы

Без N+1 (идеально):           С N+1 проблемой (плохо):
┌─────────────────┐           ┌─────────────────┐
│   1 SQL запрос  │           │   1001 запросов │
│                 │           │                 │
│ SELECT users,   │           │ SELECT users    │
│        orders   │           │ SELECT orders   │
│ FROM users      │           │ SELECT orders   │  
│ LEFT JOIN orders│           │ SELECT orders   │
│                 │           │ SELECT orders   │
└─────────────────┘           │      ...        │
     ⬇️ 10ms                   └─────────────────┘
                                    ⬇️ 5000ms

Почему N+1 происходит?

1. Lazy Loading (Ленивая загрузка)

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // По умолчанию LAZY
private List<Order> orders;

// При обращении к orders - новый SQL запрос!
user.getOrders(); // Hibernate: SELECT * FROM orders WHERE user_id = ?

2. Отсутствие JOIN в запросе

// Hibernate генерирует:
// SELECT * FROM users;  
// Без JOIN с orders!

// Потом при каждом обращении:
// SELECT * FROM orders WHERE user_id = ?;

3. Неэффективные Repository методы

// Плохо - вызовет N+1
public List<User> findAll() {
    return userRepository.findAll(); // Не включает orders
}

Способы решения N+1 проблемы

1. FETCH JOIN в JPQL

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders")
    List<User> findAllWithOrders();
    
    // Или для конкретного пользователя:
    @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
    Optional<User> findByIdWithOrders(@Param("id") Long id);
}

// Использование:
@Service
public class UserService {
    
    public void printUsersWithOrders() {
        List<User> users = userRepository.findAllWithOrders();
        // SQL: SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id = o.user_id
        // ОДИН запрос!
        
        for (User user : users) {
            System.out.println("User: " + user.getName());
            List<Order> orders = user.getOrders(); // Уже загружены!
            System.out.println("Orders count: " + orders.size());
        }
    }
}

2. @EntityGraph аннотация

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @EntityGraph(attributePaths = {"orders"})
    List<User> findAll();
    
    @EntityGraph(attributePaths = {"orders", "orders.items"}) // Вложенные связи
    Optional<User> findById(Long id);
}

// Или динамически:
@EntityGraph(value = "User.withOrders", type = EntityGraph.EntityGraphType.FETCH)
List<User> findByNameContaining(String name);

3. Named Entity Graph

@Entity
@NamedEntityGraph(
    name = "User.withOrders",
    attributeNodes = @NamedAttributeNode("orders")
)
public class User {
    // fields...
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @EntityGraph("User.withOrders")
    List<User> findAll();
}

4. Batch Fetching

@Entity
public class User {
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    @BatchSize(size = 10) // Загружает orders для 10 пользователей за раз
    private List<Order> orders;
}

// Hibernate сгенерирует:
// SELECT * FROM orders WHERE user_id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Вместо 10 отдельных запросов - 1 запрос для 10 пользователей!

5. Projection с DTO

public interface UserOrderProjection {
    Long getId();
    String getName();
    List<OrderProjection> getOrders();
    
    interface OrderProjection {
        Long getId();
        BigDecimal getAmount();
    }
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("SELECT u.id as id, u.name as name, o.id as orders_id, o.amount as orders_amount " +
           "FROM User u LEFT JOIN u.orders o")
    List<UserOrderProjection> findAllUsersWithOrders();
}

6. Criteria API с fetch

@Service
public class UserService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<User> findAllWithOrdersUsingCriteria() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        // Явный fetch join
        root.fetch("orders", JoinType.LEFT);
        
        query.select(root).distinct(true);
        
        return entityManager.createQuery(query).getResultList();
        // Один SQL запрос с JOIN!
    }
}

Сложные случаи N+1

Multiple associations

@Entity
public class User {
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
    
    @OneToMany(mappedBy = "user")  
    private List<Review> reviews;
    
    @ManyToOne
    private Department department;
}

// Решение - множественный fetch:
@Query("SELECT DISTINCT u FROM User u " +
       "LEFT JOIN FETCH u.orders " +
       "LEFT JOIN FETCH u.reviews " + 
       "LEFT JOIN FETCH u.department")
List<User> findAllWithAllAssociations();

Nested N+1 проблема

@Service
public class OrderService {
    
    // N+1 внутри N+1!
    public void printOrdersWithItems() {
        List<User> users = userRepository.findAllWithOrders(); // Решили первую N+1
        
        for (User user : users) {
            for (Order order : user.getOrders()) {
                // Новая N+1 проблема!
                List<OrderItem> items = order.getItems(); // Ленивая загрузка
                System.out.println("Items: " + items.size());
            }
        }
    }
    
    // Решение - загружаем все уровни:
    @Query("SELECT DISTINCT u FROM User u " +
           "LEFT JOIN FETCH u.orders o " +
           "LEFT JOIN FETCH o.items")
    List<User> findAllWithOrdersAndItems();
}

Инструменты для обнаружения N+1

1. Логирование SQL

# application.yml
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

2. Hibernate Statistics

@Configuration
public class HibernateConfig {
    
    @Bean
    public HibernatePropertiesCustomizer hibernatePropertiesCustomizer() {
        return (properties) -> {
            properties.put("hibernate.generate_statistics", true);
        };
    }
}

// Проверка в коде:
@Service
public class StatisticsService {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void checkQueryCount() {
        SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
            .unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();
        
        stats.clear(); // Сбрасываем счетчики
        
        // Выполняем код
        userService.printUsersWithOrders();
        
        long queryCount = stats.getQueryExecutionCount();
        System.out.println("Executed queries: " + queryCount);
        
        if (queryCount > 1) {
            System.out.println("❌ Possible N+1 problem!");
        }
    }
}

3. Spring Boot Actuator

management:
  endpoints:
    web:
      exposure:
        include: metrics
  endpoint:
    metrics:
      enabled: true

# Метрики:
# - hibernate.query.executions
# - hibernate.sessions.open  
# - hibernate.transactions

Тестирование N+1 проблем

@DataJpaTest
public class N1ProblemTest {
    
    @Autowired
    private TestEntityManager testEntityManager;
    
    @Autowired  
    private UserRepository userRepository;
    
    @Test
    public void shouldNotHaveN1Problem() {
        // Подготовка данных
        createUsersWithOrders(100);
        testEntityManager.flush();
        testEntityManager.clear();
        
        // Включаем статистику
        SessionFactory sessionFactory = testEntityManager.getEntityManager()
            .getEntityManagerFactory().unwrap(SessionFactory.class);
        Statistics stats = sessionFactory.getStatistics();
        stats.setStatisticsEnabled(true);
        stats.clear();
        
        // Тестируем код
        List<User> users = userRepository.findAllWithOrders();
        users.forEach(user -> user.getOrders().size()); // Принудительно загружаем
        
        // Проверяем количество запросов
        long queryCount = stats.getQueryExecutionCount();
        assertThat(queryCount).isEqualTo(1); // Должен быть только 1 запрос!
    }
}

Performance сравнение

@Component
public class PerformanceComparison {
    
    // N+1 проблема - медленно
    @Timed("users.with.n1.problem")
    public void loadUsersWithN1Problem() {
        List<User> users = userRepository.findAll(); // 1 запрос
        for (User user : users) {
            user.getOrders().size(); // N запросов
        }
    }
    
    // С fetch join - быстро
    @Timed("users.with.fetch.join")  
    public void loadUsersWithFetchJoin() {
        List<User> users = userRepository.findAllWithOrders(); // 1 запрос
        for (User user : users) {
            user.getOrders().size(); // 0 дополнительных запросов
        }
    }
}

// Результаты:
// N+1 проблема: 1000 пользователей = 1001 запрос = 5000ms
// Fetch join:   1000 пользователей = 1 запрос = 50ms  
// Ускорение в 100 раз!

Best Practices

1. Всегда используйте fetch joins для известных связей

// ❌ Плохо
List<User> users = userRepository.findAll();

// ✅ Хорошо  
List<User> users = userRepository.findAllWithOrders();

2. Осторожно с Cartesian Product

// ❌ Может создать огромный результат
@Query("SELECT u FROM User u " +
       "LEFT JOIN FETCH u.orders " +
       "LEFT JOIN FETCH u.reviews") // Cartesian product!
// Если у пользователя 10 orders и 5 reviews = 50 строк в результате!

// ✅ Лучше разделить на несколько запросов или использовать @BatchSize

3. Используйте Projection для read-only данных

// ✅ Для отчетов и readonly операций
public interface UserSummary {
    String getName();
    Long getOrderCount();
}

@Query("SELECT u.name as name, COUNT(o) as orderCount " +
       "FROM User u LEFT JOIN u.orders o GROUP BY u.id")
List<UserSummary> findUserSummaries();

Вопросы на интервью

Q: "Что такое N+1 проблема?" A: Когда вместо одного SQL запроса выполняется N+1: один для главных сущностей и N для связанных данных.

Q: "Как обнаружить N+1 проблему?"
A: Логирование SQL, Hibernate Statistics, мониторинг количества запросов в тестах.

Q: "Основные способы решения N+1?" A: FETCH JOIN, @EntityGraph, @BatchSize, Projection.

Q: "В чем разница между EAGER и FETCH JOIN?" A: EAGER загружает всегда (даже когда не нужно), FETCH JOIN - только в конкретном запросе.

Золотое правило: Всегда проверяйте количество SQL запросов и используйте fetch joins для известных связей!




                                                            Persistence Context

Отличный вопрос! Persistent Context - это ключевая концепция в JPA/Hibernate. Разберем детально!

Что такое Persistent Context?

Persistent Context (Контекст персистентности) - это "рабочая область" в памяти, где JPA/Hibernate отслеживает состояние всех управляемых (MANAGED) сущностей.

Простая аналогия: Persistent Context - это как "блокнот" где записаны все объекты, с которыми вы сейчас работаете, и JPA следит за всеми изменениями в этом блокноте.

Основные функции Persistent Context

1. First-Level Cache - Кеш первого уровня

@Transactional
public void demonstrateFirstLevelCache() {
    // 1-й запрос к БД
    User user1 = entityManager.find(User.class, 1L);
    // SQL: SELECT * FROM users WHERE id = 1;
    
    // 2-й запрос - БЕЗ обращения к БД!
    User user2 = entityManager.find(User.class, 1L);  
    // НЕТ SQL! Возвращается тот же объект из кеша
    
    System.out.println(user1 == user2); // true - ТОТ ЖЕ объект в памяти!
}

2. Identity Management - Гарантия уникальности

@Transactional
public void demonstrateIdentityManagement() {
    User user1 = entityManager.find(User.class, 1L);
    User user2 = userRepository.findById(1L).get();
    
    // В рамках одного Persistent Context - всегда тот же объект!
    System.out.println(user1 == user2); // true
    
    user1.setName("John");
    System.out.println(user2.getName()); // "John" - тот же объект!
}

3. Dirty Checking - Отслеживание изменений

@Transactional
public void demonstrateDirtyChecking() {
    User user = entityManager.find(User.class, 1L); // Попадает в Persistent Context
    
    // Persistent Context запоминает исходное состояние
    String originalName = user.getName(); // "John"
    
    // Изменяем объект
    user.setName("Jane");
    
    // При flush() Persistent Context сравнивает текущее и исходное состояние
    // и генерирует UPDATE только для измененных полей!
    
    // Автоматически при commit: UPDATE users SET name = 'Jane' WHERE id = 1;
}

Визуализация Persistent Context

┌─────────────────── Persistent Context ──────────────────┐
│                                                          │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │   User#1    │    │   Order#5   │    │   User#3    │  │
│  │ name="John" │    │ amount=100  │    │ name="Jane" │  │
│  │ MANAGED     │    │ MANAGED     │    │ MANAGED     │  │
│  └─────────────┘    └─────────────┘    └─────────────┘  │
│                                                          │
│  Identity Map: {1 -> User#1, 5 -> Order#5, 3 -> User#3}│
│  Dirty Entities: {User#3}                              │
│                                                          │
└──────────────────────────────────────────────────────────┘
                              │
                              ▼
                        ┌─────────────┐
                        │   Database  │
                        │   MySQL     │
                        └─────────────┘

Жизненный цикл Persistent Context

В Spring - один Persistent Context на @Transactional

@Service
public class UserService {
    
    @Transactional // ← Persistent Context СОЗДАЕТСЯ
    public void businessLogic() {
        
        // Все операции используют ОДИН Persistent Context:
        User user1 = entityManager.find(User.class, 1L);  // Загружается в контекст
        User user2 = entityManager.find(User.class, 1L);  // Из контекста (кеш)
        
        user1.setName("Updated"); // Изменение отслеживается контекстом
        
        Order order = new Order();
        entityManager.persist(order); // Добавляется в контекст
        
        // При выходе - flush() и затем контекст уничтожается
    } // ← Persistent Context УНИЧТОЖАЕТСЯ
}

Операции с Persistent Context

Добавление сущности в контекст

@Transactional
public void addToContext() {
    // NEW сущность НЕ в контексте
    User user = new User();
    user.setName("John");
    
    boolean contains1 = entityManager.contains(user); // false
    
    // Добавляем в Persistent Context
    entityManager.persist(user); // NEW → MANAGED
    
    boolean contains2 = entityManager.contains(user); // true
}

Удаление из контекста

@Transactional
public void removeFromContext() {
    User user = entityManager.find(User.class, 1L); // В контексте
    
    entityManager.detach(user); // Убираем из контекста
    
    boolean contains = entityManager.contains(user); // false
    
    user.setName("Changed"); // Изменения НЕ отслеживаются!
}

Очистка всего контекста

@Transactional
public void clearContext() {
    User user1 = entityManager.find(User.class, 1L);
    User user2 = entityManager.find(User.class, 2L);
    
    // Оба объекта в Persistent Context
    System.out.println(entityManager.contains(user1)); // true
    System.out.println(entityManager.contains(user2)); // true
    
    // Очищаем весь контекст
    entityManager.clear();
    
    System.out.println(entityManager.contains(user1)); // false
    System.out.println(entityManager.contains(user2)); // false
}

Множественные Persistent Context

Разные транзакции = разные контексты

@Service
public class MultiContextExample {
    
    @Transactional
    public void method1() {
        User user = entityManager.find(User.class, 1L); // Persistent Context #1
        user.setName("From Method 1");
    } // Контекст #1 закрывается
    
    @Transactional  
    public void method2() {
        User user = entityManager.find(User.class, 1L); // Persistent Context #2
        // Это ДРУГОЙ объект User, не тот что в method1!
        user.setName("From Method 2");
    } // Контекст #2 закрывается
    
    public void demo() {
        method1(); // Изменения сохранены в БД
        method2(); // Перезаписывает изменения из method1
    }
}

Session vs EntityManager vs Persistent Context

// Hibernate Session = JPA EntityManager = обертка над Persistent Context
@PersistenceContext
private EntityManager entityManager; // JPA стандарт

@Autowired  
private SessionFactory sessionFactory; // Hibernate специфично

@Transactional
public void comparison() {
    // Оба используют ОДИН Persistent Context!
    Session session = entityManager.unwrap(Session.class);
    
    User user1 = entityManager.find(User.class, 1L);
    User user2 = session.get(User.class, 1L);
    
    System.out.println(user1 == user2); // true - тот же объект!
}

Проблемы с Persistent Context

Проблема 1: Memory Leak в длинных транзакциях

@Transactional
public void memoryLeakExample() {
    // ❌ ПЛОХО - загружаем много объектов в контекст
    for (int i = 1; i <= 10000; i++) {
        User user = entityManager.find(User.class, (long) i);
        // Все 10000 объектов остаются в Persistent Context!
        // Память не освобождается до конца транзакции
    }
    // OutOfMemoryError возможен!
}

// ✅ РЕШЕНИЕ - периодическая очистка
@Transactional
public void memoryLeakSolution() {
    for (int i = 1; i <= 10000; i++) {
        User user = entityManager.find(User.class, (long) i);
        
        // Обрабатываем пользователя...
        
        if (i % 100 == 0) {
            entityManager.flush(); // Сохраняем изменения
            entityManager.clear(); // Очищаем контекст
        }
    }
}

Проблема 2: Stale Object State

@Transactional
public void staleObjectExample() {
    User user = entityManager.find(User.class, 1L);
    // В Persistent Context: User{id=1, name="John", version=1}
    
    // В другой транзакции кто-то изменил пользователя в БД:
    // UPDATE users SET name = 'Jane', version = 2 WHERE id = 1;
    
    // Но в нашем контексте данные устарели!
    System.out.println(user.getName()); // "John" - старые данные!
    
    // Решение - принудительная перезагрузка:
    entityManager.refresh(user);
    System.out.println(user.getName()); // "Jane" - свежие данные
}

Производительность и Persistent Context

First Level Cache экономит запросы

@Transactional
public void performanceExample() {
    // Без Persistent Context потребовалось бы 3 SQL запроса:
    User user1 = entityManager.find(User.class, 1L); // SQL запрос
    User user2 = entityManager.find(User.class, 1L); // НЕТ SQL - из кеша!
    User user3 = entityManager.find(User.class, 1L); // НЕТ SQL - из кеша!
    
    // Итого: 1 SQL запрос вместо 3!
}

Batch операции и контекст

@Transactional
public void batchOperations() {
    // Все изменения накапливаются в Persistent Context
    for (int i = 1; i <= 1000; i++) {
        User user = entityManager.find(User.class, (long) i);
        user.setName("Updated " + i);
        // НЕТ немедленных UPDATE запросов!
    }
    
    // При flush() - все UPDATE'ы отправляются batch'ем
    entityManager.flush(); // 1000 UPDATE операций за раз!
}

Monitoring Persistent Context

Hibernate Statistics

@Service
public class ContextMonitoring {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void checkContextState() {
        SessionImplementor session = entityManager.unwrap(SessionImplementor.class);
        PersistenceContext context = session.getPersistenceContext();
        
        // Количество объектов в контексте
        int entitiesCount = context.getNumberOfManagedEntities();
        System.out.println("Entities in context: " + entitiesCount);
        
        // Проверка на dirty объекты
        boolean hasDirtyEntities = context.hasNonReadOnlyEntities();
        System.out.println("Has dirty entities: " + hasDirtyEntities);
    }
}

Best Practices для Persistent Context

✅ Правильные подходы:

  1. Не держите долго большие объемы данных в контексте
  2. Используйте flush() и clear() в длинных операциях
  3. Понимайте что контекст = транзакция в Spring
  4. Используйте refresh() для получения актуальных данных

❌ Избегайте:

  1. Загрузки тысяч объектов в один контекст
  2. Работы с detached объектами без merge()
  3. Изменения объектов вне транзакции
  4. Длинных транзакций с большими данными

Вопросы на интервью

Q: "Что такое Persistent Context?" A: Рабочая область в памяти где JPA отслеживает состояние всех MANAGED сущностей, обеспечивает кеширование первого уровня и identity management.

Q: "Что происходит с Persistent Context при завершении транзакции?" A: Выполняется flush() (синхронизация с БД), затем контекст уничтожается, все объекты становятся DETACHED.

Q: "В чем разница между Session и EntityManager?" A: EntityManager - JPA стандарт, Session - Hibernate API. Оба работают с одним Persistent Context.

Q: "Зачем нужен First Level Cache?" A: Обеспечивает identity management (один объект = один экземпляр в памяти) и повышает производительность, избегая повторных запросов к БД.

Золотое правило: Persistent Context живет в рамках транзакции - планируйте работу с данными соответственно! 🎯




Отличный вопрос! Persistent Context - это ключевая концепция в JPA/Hibernate. Разберем детально!

Что такое Persistent Context?

Persistent Context (Контекст персистентности) - это "рабочая область" в памяти, где JPA/Hibernate отслеживает состояние всех управляемых (MANAGED) сущностей.

Простая аналогия: Persistent Context - это как "блокнот" где записаны все объекты, с которыми вы сейчас работаете, и JPA следит за всеми изменениями в этом блокноте.

Основные функции Persistent Context

1. First-Level Cache - Кеш первого уровня

@Transactional
public void demonstrateFirstLevelCache() {
    // 1-й запрос к БД
    User user1 = entityManager.find(User.class, 1L);
    // SQL: SELECT * FROM users WHERE id = 1;
    
    // 2-й запрос - БЕЗ обращения к БД!
    User user2 = entityManager.find(User.class, 1L);  
    // НЕТ SQL! Возвращается тот же объект из кеша
    
    System.out.println(user1 == user2); // true - ТОТ ЖЕ объект в памяти!
}

2. Identity Management - Гарантия уникальности

@Transactional
public void demonstrateIdentityManagement() {
    User user1 = entityManager.find(User.class, 1L);
    User user2 = userRepository.findById(1L).get();
    
    // В рамках одного Persistent Context - всегда тот же объект!
    System.out.println(user1 == user2); // true
    
    user1.setName("John");
    System.out.println(user2.getName()); // "John" - тот же объект!
}

3. Dirty Checking - Отслеживание изменений

@Transactional
public void demonstrateDirtyChecking() {
    User user = entityManager.find(User.class, 1L); // Попадает в Persistent Context
    
    // Persistent Context запоминает исходное состояние
    String originalName = user.getName(); // "John"
    
    // Изменяем объект
    user.setName("Jane");
    
    // При flush() Persistent Context сравнивает текущее и исходное состояние
    // и генерирует UPDATE только для измененных полей!
    
    // Автоматически при commit: UPDATE users SET name = 'Jane' WHERE id = 1;
}

Визуализация Persistent Context

┌─────────────────── Persistent Context ──────────────────┐
│                                                          │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐  │
│  │   User#1    │    │   Order#5   │    │   User#3    │  │
│  │ name="John" │    │ amount=100  │    │ name="Jane" │  │
│  │ MANAGED     │    │ MANAGED     │    │ MANAGED     │  │
│  └─────────────┘    └─────────────┘    └─────────────┘  │
│                                                          │
│  Identity Map: {1 -> User#1, 5 -> Order#5, 3 -> User#3}│
│  Dirty Entities: {User#3}                              │
│                                                          │
└──────────────────────────────────────────────────────────┘
                              │
                              ▼
                        ┌─────────────┐
                        │   Database  │
                        │   MySQL     │
                        └─────────────┘

Жизненный цикл Persistent Context

В Spring - один Persistent Context на @Transactional

@Service
public class UserService {
    
    @Transactional // ← Persistent Context СОЗДАЕТСЯ
    public void businessLogic() {
        
        // Все операции используют ОДИН Persistent Context:
        User user1 = entityManager.find(User.class, 1L);  // Загружается в контекст
        User user2 = entityManager.find(User.class, 1L);  // Из контекста (кеш)
        
        user1.setName("Updated"); // Изменение отслеживается контекстом
        
        Order order = new Order();
        entityManager.persist(order); // Добавляется в контекст
        
        // При выходе - flush() и затем контекст уничтожается
    } // ← Persistent Context УНИЧТОЖАЕТСЯ
}

Операции с Persistent Context

Добавление сущности в контекст

@Transactional
public void addToContext() {
    // NEW сущность НЕ в контексте
    User user = new User();
    user.setName("John");
    
    boolean contains1 = entityManager.contains(user); // false
    
    // Добавляем в Persistent Context
    entityManager.persist(user); // NEW → MANAGED
    
    boolean contains2 = entityManager.contains(user); // true
}

Удаление из контекста

@Transactional
public void removeFromContext() {
    User user = entityManager.find(User.class, 1L); // В контексте
    
    entityManager.detach(user); // Убираем из контекста
    
    boolean contains = entityManager.contains(user); // false
    
    user.setName("Changed"); // Изменения НЕ отслеживаются!
}

Очистка всего контекста

@Transactional
public void clearContext() {
    User user1 = entityManager.find(User.class, 1L);
    User user2 = entityManager.find(User.class, 2L);
    
    // Оба объекта в Persistent Context
    System.out.println(entityManager.contains(user1)); // true
    System.out.println(entityManager.contains(user2)); // true
    
    // Очищаем весь контекст
    entityManager.clear();
    
    System.out.println(entityManager.contains(user1)); // false
    System.out.println(entityManager.contains(user2)); // false
}

Множественные Persistent Context

Разные транзакции = разные контексты

@Service
public class MultiContextExample {
    
    @Transactional
    public void method1() {
        User user = entityManager.find(User.class, 1L); // Persistent Context #1
        user.setName("From Method 1");
    } // Контекст #1 закрывается
    
    @Transactional  
    public void method2() {
        User user = entityManager.find(User.class, 1L); // Persistent Context #2
        // Это ДРУГОЙ объект User, не тот что в method1!
        user.setName("From Method 2");
    } // Контекст #2 закрывается
    
    public void demo() {
        method1(); // Изменения сохранены в БД
        method2(); // Перезаписывает изменения из method1
    }
}

Session vs EntityManager vs Persistent Context

// Hibernate Session = JPA EntityManager = обертка над Persistent Context
@PersistenceContext
private EntityManager entityManager; // JPA стандарт

@Autowired  
private SessionFactory sessionFactory; // Hibernate специфично

@Transactional
public void comparison() {
    // Оба используют ОДИН Persistent Context!
    Session session = entityManager.unwrap(Session.class);
    
    User user1 = entityManager.find(User.class, 1L);
    User user2 = session.get(User.class, 1L);
    
    System.out.println(user1 == user2); // true - тот же объект!
}

Проблемы с Persistent Context

Проблема 1: Memory Leak в длинных транзакциях

@Transactional
public void memoryLeakExample() {
    // ❌ ПЛОХО - загружаем много объектов в контекст
    for (int i = 1; i <= 10000; i++) {
        User user = entityManager.find(User.class, (long) i);
        // Все 10000 объектов остаются в Persistent Context!
        // Память не освобождается до конца транзакции
    }
    // OutOfMemoryError возможен!
}

// ✅ РЕШЕНИЕ - периодическая очистка
@Transactional
public void memoryLeakSolution() {
    for (int i = 1; i <= 10000; i++) {
        User user = entityManager.find(User.class, (long) i);
        
        // Обрабатываем пользователя...
        
        if (i % 100 == 0) {
            entityManager.flush(); // Сохраняем изменения
            entityManager.clear(); // Очищаем контекст
        }
    }
}

Проблема 2: Stale Object State

@Transactional
public void staleObjectExample() {
    User user = entityManager.find(User.class, 1L);
    // В Persistent Context: User{id=1, name="John", version=1}
    
    // В другой транзакции кто-то изменил пользователя в БД:
    // UPDATE users SET name = 'Jane', version = 2 WHERE id = 1;
    
    // Но в нашем контексте данные устарели!
    System.out.println(user.getName()); // "John" - старые данные!
    
    // Решение - принудительная перезагрузка:
    entityManager.refresh(user);
    System.out.println(user.getName()); // "Jane" - свежие данные
}

Производительность и Persistent Context

First Level Cache экономит запросы

@Transactional
public void performanceExample() {
    // Без Persistent Context потребовалось бы 3 SQL запроса:
    User user1 = entityManager.find(User.class, 1L); // SQL запрос
    User user2 = entityManager.find(User.class, 1L); // НЕТ SQL - из кеша!
    User user3 = entityManager.find(User.class, 1L); // НЕТ SQL - из кеша!
    
    // Итого: 1 SQL запрос вместо 3!
}

Batch операции и контекст

@Transactional
public void batchOperations() {
    // Все изменения накапливаются в Persistent Context
    for (int i = 1; i <= 1000; i++) {
        User user = entityManager.find(User.class, (long) i);
        user.setName("Updated " + i);
        // НЕТ немедленных UPDATE запросов!
    }
    
    // При flush() - все UPDATE'ы отправляются batch'ем
    entityManager.flush(); // 1000 UPDATE операций за раз!
}

Monitoring Persistent Context

Hibernate Statistics

@Service
public class ContextMonitoring {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public void checkContextState() {
        SessionImplementor session = entityManager.unwrap(SessionImplementor.class);
        PersistenceContext context = session.getPersistenceContext();
        
        // Количество объектов в контексте
        int entitiesCount = context.getNumberOfManagedEntities();
        System.out.println("Entities in context: " + entitiesCount);
        
        // Проверка на dirty объекты
        boolean hasDirtyEntities = context.hasNonReadOnlyEntities();
        System.out.println("Has dirty entities: " + hasDirtyEntities);
    }
}

Best Practices для Persistent Context

✅ Правильные подходы:

  1. Не держите долго большие объемы данных в контексте
  2. Используйте flush() и clear() в длинных операциях
  3. Понимайте что контекст = транзакция в Spring
  4. Используйте refresh() для получения актуальных данных

❌ Избегайте:

  1. Загрузки тысяч объектов в один контекст
  2. Работы с detached объектами без merge()
  3. Изменения объектов вне транзакции
  4. Длинных транзакций с большими данными

Вопросы на интервью

Q: "Что такое Persistent Context?" A: Рабочая область в памяти где JPA отслеживает состояние всех MANAGED сущностей, обеспечивает кеширование первого уровня и identity management.

Q: "Что происходит с Persistent Context при завершении транзакции?" A: Выполняется flush() (синхронизация с БД), затем контекст уничтожается, все объекты становятся DETACHED.

Q: "В чем разница между Session и EntityManager?" A: EntityManager - JPA стандарт, Session - Hibernate API. Оба работают с одним Persistent Context.

Q: "Зачем нужен First Level Cache?" A: Обеспечивает identity management (один объект = один экземпляр в памяти) и повышает производительность, избегая повторных запросов к БД.

Золотое правило: Persistent Context живет в рамках транзакции - планируйте работу с данными соответственно! 🎯































Комментарии

Популярные сообщения из этого блога

IoC:ApplicationContext, BeanFactory. Bean

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java