쓰레드의 동기화
싱글쓰레드 환경에서는 프로세스 내에 하나의 쓰레드로 작업이 수행되기에 데이터의 사용에 문제가 없지만 멀티쓰레드 환경에서는 동일한 자원을 공유하여 작업을 하게 될 경우 서로의 작업에 영향을 주게 된다.
예를 들어, A와 B 쓰레드가 동일한 자원을 이용하여 작업을 진행한다고 가정해보자.
A가 해당 자원을 변경시킨 후 B가 자원을 읽으려고 한다면 원래 얻고자 했던 결과가 아니라 A가 변경시킨 자원을 얻게 된다.
이러한 문제를 방지하기 위해 자원을 방해 받지 않도록 하는 개념이 임계 영역과 Lock이다.
공유할 자원에 대한 소스코드 영역을 임계 영역으로 지정해두고, 공유 자원에 대한 Lock을 획득한 하나의 쓰레드만이 자원을 사용하도록 하는 것이다. 쓰레드가 자원 사용을 완료하여 Lock을 반납해야 다른 쓰레드가 접근할 수 있다.
이렇듯 하나의 쓰레드가 작업 중일 때 다른 쓰레드가 간섭하지 못하게 막는 것을 쓰레드의 동기화(syncronization)라고 한다.
synchronized
동기화 방법 중 가장 간단한 것은 synchronized 키워드를 이용하는 것이다.
메서드 앞에 synchronized를 붙일 경우 메서드 자체가 임계 영역으로 설정되어 메서드 호출 시점부터 영역 내의 모든 객체에 lock을 얻어 작업을 수행하게 되며, 메서드 종료시 lock을 반환한다.
또는 블럭을 사용하여 코드 일부 영역을 임계 영역으로 설정하고 객체의 참조 변수를 붙임으로써 lock을 걸고자 하는 객체를 정의할 수 있다.
// 메서드 전체를 임계 영역으로 지정
public synchronized void A() {
}
public void A() {
// 특정 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) {...}
}
그러나 위와 같은 방법 역시 완벽한 동기화는 아니다.
임계 영역 외부에서 lock을 건 객체에 접근할 수 있기 때문이다.
다음 예제를 보고 출력 결과를 예상해보자.
class Example {
private int counter = 0;
public void increment() {
synchronized(this) {
counter++;
System.out.println("Synchronized Block: " + counter);
}
}
// 동기화 블록 외부에서 counter에 접근
public int getCounter() {
return counter;
}
public static void main(String[] args) {
Example example = new Example();
// 동기화된 스레드
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
example.increment();
}
});
// 동기화되지 않은 스레드
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Non-Synchronized: " + example.getCounter());
}
});
t1.start();
t2.start();
}
}
만약 lock이 걸린 객체에 대해 외부에서 접근할 수 없다면 t1, t2 모두 1부터 10까지 출력할 것이다.
cf. 위 예제에서 this는 Example을 가리킨다.
그러나 실제 결과는 다음과 같이 출력됨을 확인할 수 있다.
synchronized를 지정한 블럭은 의도한 대로 동작하지만, 지정하지 않은 블럭은 다른 쓰레드에 의해 counter 값이 변경되어 불완전한 값을 읽은 것이다.
따라서 공유 자원에 대해 적절하게 일관된 동기화를 적용해 주어야 한다.
wait(), notify()
synchronized를 통해 동기화하여 공유 자원을 보호하는 것은 좋지만, lock을 잡고 오랫동안 반환하지 않는다면 다른 쓰레드는 무한정 대기하게 된다. 이러한 상황을 개선하기 위해 wait()과 notify()를 사용할 수 있다.
임계 영역 내 작업에 지연이 걸리거나 더이상 진행이 어려울 경우 다른 쓰레드가 lock을 획들할 수 있도록 wait()을 호출하여 쓰레드를 대기 상태로 전환한 뒤 lock을 반환하고, 해당 쓰레드가 다시 작업을 진행할 수 있는 상황이 되면 notify()를 호출하여 lock을 획득할 수 있게 대기 상태를 해제하는 것이다.
하지만 notify()를 호출하더라도 쓰레드가 순서대로 lock을 얻는 것은 아니고, 그저 waiting pool에 대기하던 임의의 쓰레드가 lock을 획득할 뿐이다. 이로 인해 운이 없는 특정 쓰레드가 계속해서 lock을 얻지 못하는 기아 현상이 발생할 수 있다.
Lock과 Condition을 이용한 동기화
Lock의 종류는 다음과 같다.
- ReentrantLock - 재진입이 가능한 lock으로 가장 일반적으로 사용된다.
- ReentrantReadWriteLock - 읽기에는 공유적이고, 쓰기에는 배타적인 lock
- StampedLock - ReentrantReadWriteLock에 낙관적인 lock의 기능 추가
-- 추후 추가 예정
'Java' 카테고리의 다른 글
Thread 구현 및 실행 (0) | 2024.10.01 |
---|