개발/Java&Kotlin

[Java] 쓰레드(Thread)의 실행제어

devhooney 2022. 9. 15. 13:01
728x90

1. 쓰레드의 생성부터 소멸

https://smujihoon.tistory.com/160

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되지 않고 실행 대기열에 저장되어 차례를 기다린다.(큐 자료구조) - FIFO
  2. 실행대기상태에 있다가 자신의 차례가 되면 실행상태
  3. 실행시간이 다 되거나 yield()를 만날 경우 다시 실행대기상태
  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block를 만나면 일시 정지 상태
  5. 일시정지시간이 다되거나, notify(), resume(), interrupt()가 호출되면 실행대기열에서 자신의 차례를 기다린다.
  6. 실행을 다 마치거나 stop()이 호출되면 쓰레드는 소멸

- 번호대로 쓰레드가 실행되는 것은 아님

 

 

2. 쓰레드의 동기화

- 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화라고 한다.

 

(1) synchronized를 이용한 동기화

- 메소드 전체를 임계 영역으로 지정

- 특정한 영역을 임계 영역으로 지정

 

class ThreadEx {
    public static void main(String args[]) {
    	Runnable r = new RunableEx();
        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 -= moeny;
        }
    }
}

class RunnableEx implements Runnable {
	Account acc = new Account();
    
    public void run () {
        while(acc.getBalance() > 0) {
            int money = (int) (Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("balance: " + acc.getBalance());
        }
    }
}


// 실행결과
// balance: 700
// balance: 400
// balance: 200
// balance: 0
// balance: -100

 

- balance가 음수가 나온다.

- 원인은 if문을 통과하고 withdraw() 전에 다른 쓰레드가 끼어들어서 withdraw()를 했기 때문

- if문 통과 시 balance=200, money=100이라고 할 때 다른 쓰레드에서 money=200인 경우의 작업이 이루어져 balance가 0인데 money=100이라서 -100이 된 것

- 이런 경우를 방지하기 위해 synchronized 사용

public synchronized void withdraw(int money) {
    if (balance >= money) {
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        balance -= moeny;
    }
}

// 또는

public void withdraw(int money) {
    synchronized(this) {
    	if (balance >= money) {
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            balance -= moeny;
        }
    }
}

 

(2) wait()과 notify()

- 동기화를 통해 공유하는 데이터를 보호할 때, 특정 쓰레드가 오랫동안 멈춰있는 있거나 오랫동안 실행되는 것도 문제가 된다.

- 그래서 wait()과 notify()로 효율적으로 쓰레드를 관리한다.

 

(3) Lock과 Condition을 이용한 동기화

- java.util.concurrent.locks 패키지에서 제공하는 동기화 방법

- 종류는 3가지

  1. Lock : 공유 자원에 한 번에 한 쓰레드만 read, write가 수행 가능하도록 제공하는 인터페이스
    • ReentrantLock : Lock의 구현체로 임계 영역의 시작과 종료 지점을 직접 명시 가능
  2. ReadWriteLock : 공유 자원에 여러 개의 스레드가 read 가능하고, write는 한 스레드만 가능한 인터페이스로 읽기를 위한 lock과 쓰기를 위한 lock을 별도로 제공
    • ReentrantReadWriteLock : ReadWriteLock의 구현체
  3. StampedLock
    • ReentrantReadWriteLock에 낙관적인 lock기능을 추가한 것
    • lock을 걸거나 해지할 때 스탬프(long 타입의 정수)를 사용하여 일긱와 쓰기를 위한 lock외에 낙관적인 lock이 추가된 것
    • 기존에는 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야 하지만, 낙관적인 읽기 lock은 쓰기 lock에 의해 바로 풀린다. 따라서 낙관적인 읽기에 실패하면 읽기 lock을 얻어와서 다시 읽어야 함
    • 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 건다.

- StampedLock을 이용한 낙관적 읽기 예시

int getBalance() {
    long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 건다.
    
    int currBalance = this.balance; // 공유 데이터인 balance를 읽어온다.
    
    if (!lock.validate(stamp)) { // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
        stamp = lock.readLock(); // lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.    	
		try {
        	currBalance = this.balance; // 공유 데이터를 다시 읽어온다.
        } finally {
        	lock.unlockRead(stamp); // 읽기 lock을 푼다.
        }
    }
    return currBalance; // 낙관적 읽기 lock이 풀리지 않으면 바로 읽어온 값을 반환.
}

 

느낀점

쓰레드를 공부해봤지만, 역시 책이나 검색으로 완벽하게 이해하는건 한계가 있는 것 같다.

사용할 기회가 있다면 사용하고 정리를 다시 한번 해봐야겠다.

 

728x90