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