JVM

쓰레드 동기화

kyoulho 2024. 3. 1. 11:38

쓰레드 동기화란?


쓰레드 동기화는 여러 쓰레드가 공유 자원에 접근할 때 발생할 수 있는 데이터 불일치와 같은 문제를 해결하는 기술이다. 이를 통해 프로그램이 예측 가능하고 안전하게 실행될 수 있도록 한다.

 

synchronized 키워드


자바에서는 synchronized 키워드를 사용하여 메소드나 블록을 동기화할 수 있다. 이를 통해 특정 코드 영역에는 하나의 쓰레드만 접근할 수 있도록 보장할 수 있다.

// 메소드 동기화
public synchronized void synchronizedMethod() {
    // 동기화가 필요한 코드
}

// 블록 동기화
public void someMethod() {
    // 비동기화 코드

    synchronized (lockObject) {
        // 동기화가 필요한 코드
    }

    // 비동기화 코드
}

 

Lock 인터페이스


 

synchronized 키워드 외에도 java.util.concurrent.locks 패키지에서 제공하는 Lock 인터페이스를 사용하여 동기화를 구현할 수 있다. 이는 더 세밀한 제어를 가능케 해주며, 특히 특정 상황에서 락을 얻지 못할 때 대기하거나, 일정 시간 동안만 락을 소유할 수 있는 장점이 있다.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Example {
    private final Lock lock = new ReentrantLock();

    public void someMethod() {
        lock.lock();
        try {
            // 동기화가 필요한 코드
        } finally {
            lock.unlock();
        }
    }
}

 

예제: Lock을 이용하여 동시에 하나의 계좌에 입금 요청이 들어올 때 예외를 발생시키는 로직

@Service
public class AccountService {

    private final Map<String, Account> accountMap = new HashMap<>();
    private final Map<String, Lock> accountLocks = new HashMap<>();

    public void deposit(String accountId, int amount) {
        Lock lock = accountLocks.get(accountId);

        if (lock.tryLock()) {
            try {
                Account account = accountMap.get(accountId);
                account.setBalance(account.getBalance() + amount);
            } finally {
                lock.unlock();  // 락 반환
            }
        } else {
            throw new RuntimeException("다른 스레드가 이미 해당 계좌에 대한 락을 가지고 있습니다.");
        }
    }

    public void withdraw(String accountId, int amount) {
        Lock lock = accountLocks.get(accountId);
        lock.lock();  // 락 획득

        try {
            Account account = accountMap.get(accountId);

            if (account.getBalance() < amount) {
                throw new RuntimeException("잔액이 부족합니다.");
            }

            account.setBalance(account.getBalance() - amount);

        } finally {
            lock.unlock();  // 락 반환
        }
    }

    public Account getAccountById(String accountId) {
        return accountMap.get(accountId);
    }
}

 

wait(), notify()


자바에서 멀티스레딩 환경에서 스레드 간 효과적인 협력을 위해 wait()notify() 메소드가 제공된다. 이들은 객체 간에 동기화와 통신을 위한 메커니즘을 제공하며, 객체의 락을 획득한 스레드가 다른 스레드에게 신호를 보내거나 기다릴 수 있게 한다.

wait() 메소드

wait() 메소드는 현재 실행 중인 스레드를 일시적으로 중지시키고, 다른 스레드가 해당 객체의 락을 획득하고 notify() 또는 notifyAll() 메소드를 호출할 때까지 기다리게 한다.

synchronized (someObject) {
    // 일부 작업 수행

    try {
        someObject.wait(); // 다른 스레드가 notify()를 호출할 때까지 기다림
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 일부 작업 수행
}

notify() 메소드

notify() 메소드는 현재 객체의 락을 획득한 다른 스레드 중 하나를 임의로 선택하여 깨우는 역할을 한다.

synchronized (someObject) {
    // 일부 작업 수행

    someObject.notify(); // wait() 중인 스레드 중 하나를 깨움

    // 일부 작업 수행
}

notifyAll() 메소드

notifyAll() 메소드는 현재 객체의 락을 획득한 모든 스레드를 깨운다.

synchronized (someObject) {
    // 일부 작업 수행

    someObject.notifyAll(); // wait() 중인 모든 스레드를 깨움

    // 일부 작업 수행
}

 

예제: 생산자-소비자 문제

public class SharedResource {
    private int data;
    private boolean newData = false;

    public synchronized void produce(int value) {
        while (newData) {
            try {
                wait(); // 소비자가 데이터를 소비할 때까지 기다림
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        data = value;
        newData = true;
        System.out.println("Produced: " + value);

        notify(); // 생산자에게 데이터를 생산했음을 알림
    }

    public synchronized int consume() {
        while (!newData) {
            try {
                wait(); // 생산자가 데이터를 생산할 때까지 기다림
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        newData = false;
        System.out.println("Consumed: " + data);

        notify(); // 소비자에게 데이터를 소비했음을 알림

        return data;
    }
}

public class Producer implements Runnable {
    private final SharedResource sharedResource;

    public Producer(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            sharedResource.produce(i);
        }
    }
}

public class Consumer implements Runnable {
    private final SharedResource sharedResource;

    public Consumer(SharedResource sharedResource) {
        this.sharedResource = sharedResource;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            sharedResource.consume();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        Thread producerThread = new Thread(new Producer(sharedResource));
        Thread consumerThread = new Thread(new Consumer(sharedResource));

        producerThread.start();
        consumerThread.start();
    }
}

이 예제에서 SharedResource 클래스는 데이터를 공유하는 클래스이다. Producer 클래스는 데이터를 생산하고, Consumer 클래스는 데이터를 소비한다. 두 스레드 간의 동기화를 위해 wait()notify()를 사용하여 데이터의 생산과 소비를 조절하고 있다.

이러한 방식으로 wait()notify()를 사용하면 스레드 간의 효과적인 협력이 가능해지며, 다양한 상황에서 동기화를 달성할 수 있다. 그러나 사용에 주의가 필요하며, 잘못된 사용은 데드락과 같은 문제를 발생시킬 수 있다. 따라서 신중한 설계와 테스트가 필요하다.

'JVM' 카테고리의 다른 글

StringBuffer vs StringBuilder  (0) 2024.07.22
java.security.invalidKeyException: Illegal Key Size  (0) 2024.05.04
자바 내부 클래스(Inner Classes)  (0) 2024.03.01
Google JIB  (0) 2024.01.30
JVM  (0) 2023.12.19