스레드의 동기화는 무엇이며, 어떻게 사용할까?
스레드의 동기화
--
멀티 스레드 프로세스는
여러 스레드가 같은 자원을 공유해서 사용하기 때문에
다른 스레드의 작업에 영향을 미칠 수가 있다.
(A스레드가 작업 도중에 B스레드로 차례가 넘어갔을 경우 B스레드에 영향을 줄 수 있다.)
이를 방지하기 위해서는 "동기화"가 필요하다.
(진행 중인 작업이 다른 스레드에게 간섭을 받지 않게 하는 것)
즉, 스레드의 동기화는
한 스레드가 진행 중이던 작업을 다른 스레드가 접근하지 못하게 막는 것이다.
동기화하는 방법은?
간섭을 받으면 안되는 코드들을 "임계 영역"으로 묶어서 설정한다.
임계 영역이란?
락(Lock)이라는 자물쇠를 얻은 단 하나의 스레드만 출입이 가능하도록 하는 영역이다.
(객체 하나당 1개의 락을 보유)
--
동기화 방법 (synchronized)
--
동기화는 "synchronized"라는 키워드를 가지고 동기화를 진행한다.
synchronized는 임계 영역을 설정할 때 사용된다.
synchronized를 통해 임계 영역 설정하는 방법 2가지
// 1. 메서드 전체를 임계 영역으로 지정하는 방법
public synchronized void calcSum() {
...
}
// 2. 특정 영역만 임계 영역으로 지정하는 방법
synchronized( 참조변수 ) {
...
}
// 1. 메서드 전체를 임계 영역으로 지정하는 방법 (withdraw() 메서드에 한 번에 하나의 스레드만 접근 가능)
public synchronized void withdraw(int money) {
if(balance >= money) {
try {
Thread.sleep(1000);
} catch(Exception e) { }
balance -= money;
}
}
// 2. 특정 영역만 임계 영역으로 지정하는 방법 (synchronized블록에 한 번에 하나의 스레드만 접근 가능)
public void withdraw(int money) {
synchronized(this) { // this는 현재 객체를 가리키는 참조변수
if(balance >= money) {
try {
Thread.sleep(1000);
} catch(Exception e) { }
balance -= money;
}
}
}
임계 영역은 한 번에 하나의 스레드만 사용할 수 있기 때문에
영역 범위와 개수를 최소화하는 것이 좋다.
왜냐하면
멀티 스레드는 여러 스레드가 동시에 작업을 하는 것이 장점인데
임계 영역에서는 한 번에 하나의 스레드만 접근이 가능하니 성능이 떨어질 수 있다.
그래서 메서드 전체를 임계 영역으로 하는 것보다 특정 부분만 임계 영역으로 하는 것이 좋을 수 있다.
예시 코드
class Account2 {
private int balance = 1000; // 잔고
public int getBalance() {
return balance;
}
// 출금하는 메서드
public synchronized void withdraw(int money) { // 동기화 (임계영역)
if(balance >= money) { // 잔고보다 작은 돈만 출금 가능
try {
Thread.sleep(1000); // 결과를 보기 쉬우라고 작성
} catch(Exception e) { }
balance -= money;
}
}
}
class RunnableEx implements Ruunable {
Account2 acc = new Account2();
public void run() {
while(acc.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100; // 랜덤 돈
acc.withdraw(money); // 출금 메서드 호출
System.out.println("balance : " + acc.getBalance()); // 출금 후 잔고 출력
}
}
}
class TreadEx {
public static void main(String args[]) {
Runnable r = new RunnableEx();
new Thread(r).start();
new Thread(r).start();
}
}
코드 결과
balance : 800
balance : 500
balance : 200
balance : 0
balance : 0
- A 스레드가 자물쇠를 가지고 임계 영역에 접근
- 그 이후에는 다른 스레드가 해당 임계 영역에 접근 불가능
- A 스레드가 임계 영역에 벗어나면 해당 임계 영역의 자물쇠를 반납하고 빠져나감
- 기다리고 있던 B스레드가 자물쇠를 가지고 임계 영역에 접근
즉, 임계 영역에는 한 번에 하나의 스레드만 접근이 가능하다.
만약 synchronized를 사용하지 않았을 경우의 코드 결과
balance : 900
balance : 700
balance : 600
balance : 400
balance : 200
balance : -100
- 마지막에서 두 번째(balance : 200) 상황에서 A 스레드가 200을 출금하려고 함
- 현재 출금할 잔고(200)가 존재하기 때문에 if문을 통과하고 1초간 sleep()으로 대기 상태 진입
- 그 사이에 B 스레드가 100을 출금하려고 접근할 때
아직도 잔고는 200이 남아있기 때문에 B 스레드 또한 if문을 통과하고 sleep()으로 대기 상태 진입 - A 스레드는 sleep() 상태가 끝나고 200을 출금하여 잔고는 0
- B 스레드가 sleep() 상태가 끝나고 100을 출금하여 잔고는 -100
synchronized로 선언된 임계영역은
아무리 많이 존재해도 그 중에서 하나의 임계 영역에 하나의 스레드가 진입한 경우
나머지 임계 영역도 모두 락이 걸려 다른 스레드들은 나머지 임계 영역에도 접근이 불가능하다.
임계 영역에는 작업 도중 다른 스레드가 접근하면 안 되는 코드들이 존재하는데
해당 코드들이 다른 임계 영역에도 존재할 수 있기 때문에 이를 애초에 방지하기 위해서
임계 영역이 많더라도 그 중에서 하나의 스레드가 접근하면 나머지 임계 영역들도 전부 락이 걸린다.
이것이 synchronized 키워드의 동작 원리다.
--
wait(), notify()
--
wait()과 notify()는
동기화의 효율을 높이기 위해 사용되는 기능이다.
동기화를 사용하면 데이터는 보호가 되지만
한 번에 하나의 스레드만 접근이 가능하므로 비효율적이기 때문이다.
- wait() : 객체의 락(lock)을 풀고 스레드를 해당 객체의 waiting pool에 넣는다.
- notify() : waiting pool에서 대기 중인 스레드 중의 하나를 깨운다.
- notifyAll() : waiting pool에서 대기중인 모든 스레드를 깨운다.
wait()과 notify()는 Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용이 가능하다.
wait()과 notify()를 사용하지 않았을 경우의 예시 코드
class Account {
int balance = 1000; // 잔고
// 출금 메서드
public synchronized void withdraw(int money) {
while(balance < money) { // 잔고가 출금한 돈보다 적다면
...
}
balance -= money;
}
// 입금 메서드
public synchronized void deposit(int money) {
balance += money;
}
}
위 코드에서는
출금 임계 영역에 접근했지만 출금할 만큼의 잔고가 없다면 while문에서 계속 반복하여 머물게 될 것이다.
그러면 다음 스레드는 계속 해당 출금 임계 영역에 접근이 불가능 할 것이고,
임금 메서드 또한 임계 영역이므로 락이 걸려있기 때문에 접근이 불가능하게 된다.
wait()과 notify()를 사용한 경우의 예시 코드
class Account {
int balance = 1000; // 잔고
// 출금 메서드
public synchronized void withdraw(int money) {
while(balance < money) { // 잔고가 출금한 돈보다 적다면
try {
wait(); // 해당 스레드를 waiting pool로 이동 시킴
} catch(InterruptedException e) { }
}
balance -= money;
}
// 입금 메서드
public synchronized void deposit(int money) {
balance += money;
notify(); // waiting pool에 대기 중인 스레드 중에서 하나 깨움l에 대기 중인 스레드 중에서 하나 깨움
}
}
- A 스레드가 출금 임계 영역에 진입 (모든 임계 영역이 락 걸림)
- A 스레드가 wait()을 만나 임계 영역을 빠져나와서 waiting pool(대기실)에 들어감 (모든 임계 영역의 락이 풀림)
- B 스레드가 입금 임계 영역에 진입 후 notify()를 통해 waiting pool에 존재하는 스레드 중 하나에게 나오라고 알림
이때 B 스레드는 임계 영역에서 빠져나온다. - notify() 알림을 받은 A 스레드는 waiting pool에서 빠져나와서 진행 중이었던 wait() 부분의 다음부터 이어서 진행
이렇게
스레드가 작업을 하다가 작업을 수행할 상황이 안된다면
wait()을 통해 잠깐 중단하여 임계 영역의 락을 풀고 waiting pool에 대기하다가
누군가 notify()로 나오라고 알려주게 될 때 다시 이어서 진행하게 된다.
notify()는 waiting pool에 대기하는 스레드들 중에서 랜덤으로 하나를 깨우게 된다.
즉, 아무리 notify()를 해도 운이 좋지 않다면 특정 스레드는 계속 깨우지 못할 수 있다는 것이다.
이를 "기아 현상"이라고 부른다.
그래서 waiting pool에 많은 스레드가 대기하고 있다면 그냥 notifyAll()을 사용하여 모두 깨우는 것이 좋을 수 있다.
하지만 여기서도 여러 스레드를 한 번에 깨우면 임계 영역의 락(lock)을 얻기 위해 서로 경쟁을 하게 되는데
이를 "경쟁 상태"라고 부른다.
그래서 경쟁 상태를 개선하기 위해서는 각 역할에 맞는 스레드를 구별해서 통지하는 것이 필요하다.
(이때 lock과 condition을 이용하여 통지할 수 있다.)
--
'Language > Java' 카테고리의 다른 글
람다식 (+ 메서드 참조, 함수형 인터페이스) (0) | 2025.01.12 |
---|---|
스레드의 상태 종류 & 제어 (0) | 2025.01.08 |
스레드 (Thread) (0) | 2024.12.29 |
제네릭 (Generics) (0) | 2024.12.28 |
Arrays 클래스에서 제공하는 메서드 (0) | 2024.12.25 |