쓰레드의 실행 제어
쓰레드의 상태
상태 | 설명 |
NEW | 쓰레드가 실행되고 아직 start()가 호출되지 않은 상태 |
RUNNABLE | 실행 중 또는 실행 가능한 상태 |
BLOCKED | 동기화블럭에 의해서 일시정지된 상태 ( lock이 풀릴 때까지 기다리는 상태 ) |
WAITING, TIMED_WAITING |
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은 (unrunnable) 일시정지 상태, TIMED_WAITING은 일시정지시간이 지정된 경우를 의미한다. |
TERMINATED | 쓰레드의 작업이 종료된 상태 |
쓰레드의 스케줄링과 관련된 메서드
메서드 | 설명 |
static void sleep(long mills), static void sleep(long mills, int nanos) |
지정된 시간( 천분의 일초 단위 )동안 쓰레드를 일시정지시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다. (현재 실행중인 쓰레드를 정지시킴) |
void join(), void join(long mills), void join(long mills, int nanos) |
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다. |
void interrupt() | sleep()이나 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 된다. |
void stop() | 쓰레드를 즉시 종료시킨다. |
void suspend() | 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기상태가 된다. |
void resume() | suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다. |
static void yield() | 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기상태가 된다. |
쓰레드의 동기화( synchronization )
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 뜻합니다.
임계 영역(critical section)과 잠금(락,lock)을 사용한 동기화
- 쓰레드가 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 합니다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 됩니다.
JAVA에서의 쓰레드 동기화 방법
1. synchronized를 이용한 동기화
- 가장 간단한 방법으로 synchronized 키워드를 사용하여 임계 영역을 설정하여 동기화하는 방법입니다.
- 임계 영역을 설정하기만 하면 lock의 획득과 반납이 자동적으로 수행됩니다.
- 메서드 전체를 임계 영역으로 지정하는 방법과 메서드 내의 특정한 영역을 임계 영역으로 지정하는 방법이 있습니다.
메서드 앞에 synchronized를 붙이는 방법
// 메서드 전체를 임계 영역으로 지정
public synchronized void func(){
...
}
- 메서드 전체를 임계 영역으로 지정하는 방법
- 쓰레드는 synchronized메서드가 호출되는 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환합니다.
메서드 내의 코드 일부에 synchronized를 붙이는 방법
// 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수){
...
}
- 메서드 내의 특정한 영역을 임계 영역으로 지정하는 방법
- 메서드 내의 코드 일부를 블럭 {}으로 감싸고 블럭 앞에 'synchronized(참조변수)'를 붙여 synchronized 블럭을 만드는 방법입니다.
- 쓰레드는 이 블럭의 영역 안으로 들어가면서부터 지정된 객체의 lock을 획득하고, 블럭을 벗어나면 lock을 반납합니다.
임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다 synchronized블럭으로 임계 영역을 최소화하는 것을 권장합니다.
동기화 전 코드
public class ThreadEx21 {
public static void main(String[] args) {
RunnableEx22 r = new RunnableEx22();
new Thread(r).start();
new Thread(r).start();
}
}
class Account{
private int balance = 1000;
public int getBalance() {
return balance;
}
public void withdraw(int money) {
if (balance >= money) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
balance -= money;
}
}
}
class RunnableEx22 implements Runnable {
Account acc = new Account();
@Override
public void run() {
while (acc.getBalance() > 0) {
int money = (int) (Math.random() * 3 + 1) * 100;
acc.withdraw(money);
System.out.println(Thread.currentThread().getName() + " balance : " + acc.getBalance());
}
}
}
- 계좌의 잔금을 확인하여 출금하는 메서드 withdraw()를 구현했습니다.
- 여기서 동기화를 해주지 않았기 때문에, 하나의 쓰레드에서 조건식 if( balance >= money )를 통과하고 출금하기 직전에 다른 쓰레드가 끼어들어 출금을 먼저 하는 경우가 발생할 수 있습니다.
실행 결과
Thread-0 balance : 800
Thread-1 balance : 600
Thread-0 balance : 300
Thread-1 balance : 300
Thread-0 balance : 0
Thread-1 balance : -300 // 잔금이 음수가 되는 문제가 발생
- Thread1이 withdraw()를 호출하여 조건문을 통과한 순간 Thread0가 출금을 해서 잔금이 음수가 되는 문제가 발생합니다.
synchronized 키워드를 이용하여 동기화한 코드
// synchronized 키워드를 사용하여 withdraw 메서드 동기화
public synchronized void withdraw(int money) {
if (balance >= money) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
balance -= money;
}
}
- withdraw에 synchronized 키워드를 사용하여 한 번에 하나의 쓰레드만 코드 영역을 수행할 수 있게 변경하였습니다.
실행 결과
Thread-0 balance : 700
Thread-1 balance : 400
Thread-0 balance : 200
Thread-1 balance : 0
Thread-0 balance : 0
2. wait() & notify()를 이용한 더 효율적인 동기화
synchronized로 동기화하는 경우 공유 데이터를 보호할 수 있다는 장점이 있습니다. 하지만 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요합니다.
( 예를 들어 계좌에 출금할 돈이 부족해서 한 쓰레드가 락을 보유한 채로 돈이 입금될 때까지 오랜 시간을 보낸다면, 다른 쓰레드들은 모두 해당 객체의 락을 기다려야 합니다. )
이런 상황을 개선하기 위해 고안된 것이 바로 wait()과 notify()입니다.
wait()
- Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다.
- 호출하면 실행 중이던 쓰레드는 해당 객체의 락을 반납하고, 해당 객체의 waiting pool에서 통지를 기다립니다.
- 매개변수가 있는 wait()은 매개변수로 지정된 시간동안만 기다립니다.
notify()
- Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다.
- 해당 객체의 waiting pool에 있던 모든 쓰레드 중에서 임의의 쓰레드에게 통지를 하는 메서드입니다.
( 대기했던 순서와 상관없이 작업을 중단했던 쓰레드 중 하나가 다시 락을 얻어 작업을 진행하게 합니다. )
notifyAll()
- Object 클래스에 정의되어 있으며, 동기화블록 내에서만 사용할 수 있습니다.
- 해당 객체의 waiting pool에 기다리고 있는 모든 쓰레드에게 통지를 하는 메서드입니다.
( 모든 쓰레드가 통지를 받지만 결국 lock은 하나의 쓰레드만 얻을 수 있습니다. )
waiting pool은 객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것은 아닙니다.
기아 현상( starvation )과 경쟁 상태( race condition )
- notify()는 임의의 쓰레드를 깨우기 때문에 어떤 쓰레드는 매우 오랫동안 기다리게 될 수도 있습니다.
이처럼 쓰레드가 계속 대기 상태에 머무는 것을 '기아 현상( starvation )'이라고 합니다. - notifyAll()을 사용하면 모든 쓰레드에게 통지를 하여 기아 현상을 해결할 수 있습니다.
하지만 모든 쓰레드가 하나의 lock을 얻기 위해 경쟁하게 되며, 이처럼 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것을 '경쟁 상태( race condition )'라고 합니다. - 경쟁 상태를 개선하기 위해서는 임의의 쓰레드에게 통지하는 것이 아니라 쓰레드를 구별해서 통지하는 선별적인 통지가 필요합니다.
3. Lock & Condition을 이용한 선별적인 동기화
lock 클래스의 종류
ReentrantLock 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가한 lock
ReentrantLock
- 가장 일반적인 lock입니다.
- 'reentrant(재진입할 수 있는)'이 붙은 것처럼 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행할 수 있습니다.
ReentrantReadWriteLock
- 읽기를 위한 lock과 쓰기를 위한 lock을 제공합니다.
- 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있습니다.
( 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드가 읽어도 문제가 되지 않습니다. ) - 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않습니다.
( 반대의 경우도 마찬가지로 쓰기 lock이 걸린 상태에서 읽기 lock을 거는 것은 허용되지 않습니다. )
StampedLock
- lock을 걸거나 해지할 때 '스탬프( long타입의 정수값 )'를 사용하며, 읽기와 쓰기를 위한 lock외에 '낙관적 읽기 lock(optimistic reading lock)'이 추가된 lock입니다.
- 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하지만 '낙관적 읽기 lock'은 쓰기 lock에 의해 바로 풀린다는 특징이 있습니다.
따라서 '낙관적 읽기 lock'를 사용할 때는 다른 쓰기 lock에 의해 낙관적 읽기가 실패했는지 확인하고 실패했다면 읽기 lock을 얻어서 다시 읽어 오도록 해야합니다.
( 즉, 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때 쓰기가 끝난 후에 읽기 lock을 거는 것입니다. )
StampedLock을 사용한 낙관적 읽기의 예
int getBalance(){
long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 겁니다.
int curBalance = this.balance; // 공유 데이터인 balance를 읽어옵니다.
if(!lock.validate(stamp)){ // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인합니다.
stamp = lock.readLock(); // 만약 lock이 풀린 경우, 읽기 lock을 얻기 위해 기다립니다.
try{
curBalance = this.balance; // 공유 데이터를 읽어옵니다.
} finally{
lock.unlockRead(stamp); // 읽기 lock을 풉니다.
}
}
}
ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair)
- 생성자의 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득할 수 있게 공정(fair)하게 처리합니다.
- 공정하게 처리하려면 어떤 쓰레드가 가장 오래 기다렸는지 확인하는 과정을 거칠 수밖에 없으므로 성능은 떨어집니다.
- lock클래스들은 synchronized 키워드를 사용할 때와 달리 수동으로 lock의 잠금과 해제를 해야합니다.
lock 잠금 & 해제 메서드
void lock() lock을 잠그는 메서드
void unlock() lock을 해지하는 메서드
boolean isLocked() lock이 잠겼는지 확인하는 메서드
- 임계 영역에 들어가기 전 lock()을 호출하고, 임계 영역을 벗어날 때 unlock() 메서드를 호출하면 됩니다.
- 임계 영역 내에서 예외가 발생하거나 return 문으로 빠져나가게 되면 lock이 풀리지 않을 수 있으므로 unlock()은 일반적으로 try - finally문으로 감싸는 방식을 사용합니다.
ReentrantLock lock = new ReentrantLock();
lock.lock(); // lock을 잠그기
try{
// 임계 영역
}finally{
lock.unlock(); // lock을 해지하기
}
응답성이 중요한 경우 사용하는 tryLock()
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
- lock()은 lock을 얻을 때까지 쓰레드를 블락(block)시키므로 쓰레드의 응답성이 나빠질 수 있습니다.
- tryLock() 메서드는 lock()과 달리, 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않습니다.
또는 지정된 시간만큼만 기다립니다. - lock을 얻으면 true, 얻지 못하면 false를 반환합니다.
ReentrantLock과 Condition
- Condition은 wait() & notify()와 달리 쓰레드의 종류에 따라 구분하여 통지를 할 수 있는 기능입니다.
- 이미 생성된 lock으로부터 newCondition()을 호출하여 생성할 수 있습니다.
- 쓰레드를 대기시키거나 깨울 때 메서드로 wait() & notify()대신 await() & signal()을 사용합니다.
- 경우에 따라 여러 개의 Condition을 생성하여 세분화할 수 있습니다.
ex)
Condition forCook = lock.newCondition(); // Cook을 위한 Condition
Condition forCust = lock.newCondition(); // Customer을 위한 Condition
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 스레드 대기
lock.lock();
try {
while (조건이 충족되지 않을 때) {
condition.await();
}
// 조건이 충족되면 작업 수행
} finally {
lock.unlock();
}
// 스레드가 조건을 충족했을 때 다른 스레드를 깨울 때
lock.lock();
try {
condition.signal(); // 하나의 스레드를 깨움
// 또는
condition.signalAll(); // 모든 스레드를 깨움
} finally {
lock.unlock();
}
'Java' 카테고리의 다른 글
[ Java의 정석 ] 쓰레드 ( Thread ) [ 1 ] (0) | 2023.08.27 |
---|---|
[ JAVA의 정석 ] 지네릭스 ( Generics ) (0) | 2023.08.13 |
[ Java 의 정석 ] 내부 클래스 ( Inner class ) (0) | 2023.08.03 |
[ Java의 정석 ] 인터페이스 ( interface ) (0) | 2023.08.01 |
[ Java의 정석 ] 추상클래스 ( abstract class ) (0) | 2023.07.29 |