[JAVA] Thread - 2

Coastby·2022년 11월 6일
0

JAVA

목록 보기
29/33

6. 쓰레드 그룹 (Thread Group)

  • 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것
  • 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 한다.
  • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 ‘main 쓰레드 그룹’에 속한다.
  • 자신을 생성한 쓰레드 (부모 쓰레드)의 그룹과 우선순위를 상속받는다.
//Thread 생성자
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
...
//관련 메서드

//쓰레드 자신이 속한 쓰레드 그룹을 반환한다 (Thread class)
ThreadGroup getThreadGroup()    

 //처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다. (ThreadGroup class)
//overriding하여 다른 동작을 하도록 할 수 있다
void uncaughtException(Thread t, Throwable e)   

7. 데몬 쓰레드 (Daemon Thread)

  • 일반 쓰레드 (non-daemon thread)의 작업을 돕는 보조적인 역할을 수행
  • 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
  • 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용된다.
  • 무한루프와 조건문을 이요해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
public void run(){
		//일반쓰레드가 종료되면 자동적으로 종료되기 때문에 무한루프 사용해도 괜찮다.
		while(true){
				try{
						Thread.sleep(3 * 1000);    //3초마다 실행
				} catch(InterruotedException e){}
				
				//autoSave의 값이 true이면 autoSave()를 호출한다.
				if(autoSave){
						autoSave();		
				}
		}
}
  • 데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하면 된다.
boolean isDaemon()    //쓰레드가 데몬 쓰레드인지 확인한다 데몬이면 true
void setDaemon(boolean on)    //쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경

💡 setDaemon()은 반드시 start()를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException 발생한다.

8. 쓰레드의 실행제어

○ 쓰레드의 상태

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. 실행대기열은 큐 (Queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
  2. 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 된다.
  3. 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다.
  5. 지정된 일시정지시간이 다되거나 (time-out), notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

○ 쓰레드의 실행제어

  • 쓰레드의 실행을 제어할 수 있는 메서드가 제공된다.
  • 이 들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.

  • static이 붙은 메서드는 쓰레드 자기 자신에게만 호출 가능하다.

9. 쓰레드의 동기화

  • 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
  • 진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 동기화가 필요하다.

💡 쓰레드의 동기화
한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것

  • 동기화하려면 간섭받지 않아야 하는 문장들을 ‘임계 영역 (critical section)’으로 설정 (공유 데이터를 사용하는 코드 영역)
  • 임계 영역은 락(Lock)을 얻은 단 하나의 쓰레드만 출입가능 (객체 1개에 락 1개)
  • 임계 영역 내의 모든 코드를 수행하고 벗어나면 lock을 반납하고 다른 쓰레드가 반납된 lock을 획득하여야 임계 영역의 코드를 실행할 수 있다.
  • 임계 영역은 가능한 최소화하는 것이 좋다.

○ synchronized를 이용한 동기화

synchronized로 임계영역 (lock이 걸리는 영역)을 설정하는 방법 2가지
1. 메서드 전체를 임계 영역으로 지정
: 반환타입 앞에 synchronized 키워드를 붙여준다.

public synchronized void calcSum(){
	//...
}
  1. 특정한 영역을 임계 영역으로 지정
synchronized(객체의참조변수){
    //...
}

○ 예제

Account class에서 출금 메서드 (withdraw())에서 잔고가 출금하려는 금액보다 클 때에만 출금이 가능하도록 되어있다. 하지만 synchronized 키워드를 사용하지 않는다면 멀티쓰레드로 실행하면 음수가 나오는 것을 볼 수 있다.

이는 한 쓰레드가 if문 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.

이처럼 한 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.

public class SynchronizedEx2 {
    public static void main(String[] args) {
        Runnable r = new SynchronizedEx2_1();
        new Thread(r).start();
        new Thread(r).start();
    }
}

class Account {
    private int balance = 1000;

    public (synchronized) int getBalance() {
        return balance;
    }

    public (synchronized) void withdraw(int money) {
        if (balance >= money) {
						//조건문을 통과하고 다른 쓰레드에게 넘겨주기 위해 sleep()을 이용
            try {Thread.sleep(1000);} catch (InterruptedException e) {}
            balance -= money;
        }
    }//withdraw
}

class SynchronizedEx2_1 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("잔액: " + acc.getBalance());
        }
    }//run
}
//result
잔액: 700
잔액: 700
잔액: 500
잔액: 500
잔액: 200
잔액: -100

wait(), notify()

동기화로 공유 데이터를 보호하는 것은 좋으나, 특정 쓰레드가 객체의 락을 가진 상태로 오래 있는 것은 비효율적이다. 다른 쓰레드들이 해당 객체를 기다리느라 다른 작업들도 원활히 진행되지 않을 것이기 때문이다.

동기화의 효율을 높이기 위해 wait(), notify()를 사용

  • Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
  • wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
  • notify() : waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
    • 재수없으면 어떤 쓰레드는 계속 기다리게 된다.
  • ✅ notifyAll() : waiting pool에서 대기중인 모든 쓰레드를 깨운다.
    • 일반적으로 모두를 깨워도 어차피 lock을 하나만 가지게 되므로, notifyAll()을 더 많이 사용한다.
class Account{
    int balance = 1000;
 
    public synchronized void withdraw(int money){
        /* 잔고가 부족할 경우 wait()를 호출하여 lock을 풀고 waiting pool에
           들어가면서 락을 다른 쓰레드에게 양보하게 된다. */
        while(balance < money){ 
            try{
                wait();
            }catch(InterruptedException e){ }
        }
 
        balance -= money;
    }
    
    /* 다른 쓰레드에 의해서 deposit()메서드가 호출되어 잔고가 증가하면서 notify()를 
       호출하면 객체의 waiting pool에서 기다리고 있던 쓰레드를 깨우게 된다. */
    public synchronized void deposit(int money){
        balance += money;
        notify();
    }
}

❌ 문제점

1) wait() & notify()는 쓰레드의 종류를 구분하지 않고, 공유 객체의 waiting pool에 같이 몰아넣는다. 그리고 notify()를 대상을 구분하지 않고 통지한다는 문제가 있다.

불필요하게 자신이 원하는 상태가 아닐 때도 깨어나서 lock을 얻기 위해 경쟁하게 된다. 이처럼 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것을 ‘경쟁 상태 (race condition)’라고 하는데, 이 경쟁 상태를 개선하기 위해서는 쓰레드를 구별해서 통지하는 것이 필요하다.

2) 지독히 운이 나쁘면 쓰레드는 계속 통지 받지 못하고 오랫동안 기다리게 되는데, 이를 ‘기아 (starvation) 현상’이라고 한다. 이 현상을 막으려면 notify() 대신 notifyAll()을 사용한다.

이러한 문제를 해결하기 위해 LockCondition을 이용할 수 있다.

profile
훈이야 화이팅

0개의 댓글