CS Study 7주차: [Java] Thread

hjern·2024년 4월 2일
0

CS Study

목록 보기
6/10

프로세스와 스레드(Process & Thread)

  • 프로세스 : 실행중인 프로그램, 자원(Resource)과 스레드로 구성
  • 스레드 : 프로세스 내에서 실제 작업을 수행하며, 모든 프로세스는 최소 하나의 스레드를 가지고 있음
  • 프로세스 : 스레드 = 공장 : 일꾼

멀티프로세스 vs. 멀티스레드

  • 멀티태스킹(멀티프로세싱) : 동시에 여러 프로세스를 실행시키는 것
  • 멀티스레딩 : 하나의 프로세스 내에 여러 스레드를 동시에 실행시키는 것으로, 프로세스를 생성하는 것보다 스레드를 생성하는 비용이 적다는 점과 같은 프로세스 내의 스레드들이 자원을 서로 공유한다는 특징이 있음

멀티스레드의 장단점

  • 대부분의 프로그램이 멀티스레드로 작성되어 있지만, 항상 장점만 있는 건 아니다.
  • 장점 : 시스템 자원을 보다 효율적으로 사용할 수 있으며 사용자에 대한 응답성(responseness)이 향상된다. 작업이 분리되어 코드가 간결해져 여러 모로 좋은 편이다.
  • 단점 : 동기화(synchronization)에 주의해야 하며 교착 상태(dead-lock) 이 발생하지 않도록 주의해야 한다. 각 스레드가 효율적으로 고르게 실행될 수 있도록 설계해야 한다. 즉, 고려할 사항들이 많다.

스레드의 구현과 실행

1. Thread 클래스를 상속

class MyThread extends Thread {
	@Override
	public void run () { // Thread 클래스의 run()을 오버라이딩
    /* 작업 내용 */
	}
}

MyTread t1 = new MyThread(); // 스레드 생성
t1.start();

2. Runnable 인터페이스를 구현

Runnable 인터페이스를 구현한 경우는, 해당 클래스를 인스턴스화해서 Thread 생성자에 argument로 넘겨줘야 한다. 그리고 run()을 호출하면 Runnable 인터페이스에서 구현한 run()이 호출되므로 따로 오버라이딩하지 않아도 되는 장점이 있다.

class MyThread2 implements Runnable {
	@Override
	public void run () { // Runnable 인터페이스의 추상메서드 run()을 구현
    /* 작업 내용 */
	}
}
    
Runnable r = new MyThread2();
Thread t2 = new Thread(r); // Thread(Runnable r)
// Thread t2 = new Thread(new MyThread2 ());
t2.start();

3. start()와 run()


main() 메서드 에서 start() 메서드가 실행되면 새로운 호출 스택을 생성하고, 새롭게 호출된 스택에서 run() 메서드를 실행시킨 뒤, start() 메서드는 종료한다. 스레드가 분리되며 각각의 호출 스택을 갖게 된다고 볼 수 있다.

스레드의 상태


1. NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태
2. RUNNABLE : 실행 중 또는 실행 가능 상태
3. BLOCKED : 동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다림)
4. WAITING, TIME_WAITING : 스레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지 상태, TIME_WAITING은 일시정지시간이 지정된 경우를 의미
5. TERMINATED : 스레드 작업이 종료된 상태

스레드의 실행제어 메서드 - sleep()

  • 현재 스레드를 지정된 시간동안 '멈추게(잠자게)' 한다
  • yield() 메서드와 함께 static 이 붙는데, 실행하는 스레드 자기 자신에게만 호출할 수 있기 때문이다(자기 외 스레드에 적용할 수 없다).
static void sleep(long millis) // 천 분의 일 초 단위
static void sleep(long millis, int nanos) // 천 분의 일 초 + 나노 초
  • time up(시간 종료) 또는 interrupted(깨워짐) 되어야 sleep() 종료되는데, 예외 처리를 해야 자연스럽게 try 문을 빠져나갈 수 있다.
try {
	Thread.sleep(1, 500000); // 스레드를 0.0015초 동안 멈추게 한다
} catch(InterruptedException e) {}
  • 항상 예외 처리 하기가 번거롭기 때문에,
void delay(long millis) {
	try {
    	Thread.sleep(millis);
        } catch(InterruptedException e) {}
}

를 사용해서

delay(15);

식으로 사용할 수 있다. 이때 Thread.sleep을 사용하면 메인이 sleep 하는 것이고, th1.sleep 하면 th1 스레드가 sleep 하는 것을 의미해 보이지만 실제론 메인 스레드가 잠드는 것이다. 작성 방법상 에러가 발생하진 않지만 메인 스레드가 sleep 하는 목적으로 사용한다면(무엇보다 다른 스레드를 잠자게 하는 기능이 아니다) Thread.sleep() 으로 사용하도록 주의해야 한다.

스레드의 실행제어 메서드 - interrupt()

  • 대기상태(WAITING, 스레드의 작업이 중단된 상태)인 스레드를 실행대기 상태(RUNNABLE)로 만든다.
    1. void interrupt() 스레드의 interrupted 상태를 false 에서 true 로 변경
    2. boolean isInterrupted() 스레드의 interrupted 상태를 반환
    3. static boolean interrupted() 현재 스레드의 interrupted 상태를 알려주고, false 로 초기화
  • th1이 interrupted 되었기 때문에 'th1.isInterrupted()'는 true를 반환한다.
class ThreadEx13_2 extends Thread {
	public voide run ()
    	...
    	while(downloaded && !isInterrupted()){
        	// download 를 수행한다.
        ...
        }
        
        System.out.println("다운로드가 끝났습니다.");
	}
}
  • 다운로드를 완료되거나 취소 버튼을 선택했을 때(interrupted() 호출) '!isInterrupted()'는 false 로 while 문을 벗어나게 된다.

스레드의 실행제어 메서드 - suspend(), resume(), stop()

  • 스레드의 실행을 일시정지, 재개, 완전 정지 시킨다.
  • 교착 상태를 만들 가능성이 있어서 사용이 지양(deprecated) 된다.

스레드의 실행제어 메서드 - join()

  • 지정된 시간 동안 특정 스레드가 작업하는 것을 기다린다.
  • 다른 스레드가 먼저 동작하도록 하게 한다.
    1. void join() // 작업이 모두 끝날 때까지
    2. void join(long millis) // 천 분의 일 초 동안
    3. void join(long millis, int nanos) // 천 분의 일 초 + 나노 초 동안
  • 예외처리를 해야 한다.(InterruptedException이 발생하면 작업 재개)
  • 10초 마다 gc()를 실행하도록 하는 데몬 스레드
  • 메모리가 부족한 경우, 잠자고 있는 스레드 gc를 깨운다.
  • 하지만 gc가 작업할 시간을 주어야 메모리가 생기기 때문에, 그 사이 join()을 넣어서 gc가 활동할 시간을 번다.

스레드의 실행제어 메서드 - yield()

  • 남은 시간을 다음 스레드에게 양보하고, 자신(현재 스레드)은 실행대기한다.
  • static 메서드이기 때문에 자기 자신에게만 사용할 수 있다.
  • yield() 와 interrupt() 를 적절히 사용하면, 응답성과 효율성을 높일 수 있다.
  • yield() 를 사용한다고 해도 OS 스케줄러에게 통보하는 방식이기 때문에 반드시 yield() 메서드가 작동한다는 보장은 없다.

동기화(Synchronization)

  • 동기화 : 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하게 막는 것
  • 여러 스레드가 같은 자원(메모리)을 공유하기 때문에 하나의 스레드 작업이 완료되지 못하고, 다른 작업으로 넘어갔을 때 다른 스레드의 작업에 영향을 미칠 수 있다. 즉, 멀티 스레드 프로세스에서는 다른 스레드의 작업에 영향을 미칠 수 있다.
  • 진행중인 작업이 다른 스레드에게 간섭받지 않게 하기 위해서 '동기화(Syncronization)'이 필요하다.
  • 동기화 하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정하며 이 임계 역역에는 lock을 얻은 단 하나의 스레드만 출입할 수 있는데, 객체 1개당 락은 1개 이다.

1. syncronized 를 이용한 동기화

1) 메서드 전체를 입계 영역으로 지정하고 싶은 경우 -- 반환 타입 앞에 syncronized
2) 특정 영역을 임계 영역으로 지정하고 싶은 경우 -- syncronized(객체의 참조 변수){}

  • syncronized 가 없다면 A 스레드가 if문을 통과한 상태에서 B 스레드에게 작동을 넘겨줬을 때, B 스레드가 먼저 withdraw() 를 통과해 나갈 수 있다. 이때, 다시 A 스레드로 넘어오면 A는 if문을 통과한 상태이므로, withdraw() 를 수행하게 될 것이고 그렇다면 잔고가 마이너스가 나오는 상황이 발생할 수 있다.
  • 이런 상황을 막기 위해서 임계 영역을 설정하고, B 스레드가 withdraw() 메서드를 통과하지 못하도록 만든다.
  • A 스레드가 만든 임계 영역의 수행할 코드가 완료되면 lock은 반납된다.

2. wait() 과 notify(), nofifyAll()

  • 동기화는 한 번에 한 스레드만 임계 영역에 들어갈 수 있어서 데이터를 보호하는 장점이 있지만, 결국 멀티 스레드를 사용한다는 장점을 살릴 수 없다는 단점이 있다.
  • 이러한 동기화의 효율을 높이기 위해 wait(), notify() 를 사용할 수 있다.
  • Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
  • A 스레드(withdraw lock 주체) 의 lock을 풀고 waiting pool(대기실) 로 잠시 옮겨둔다. 그 사이 B 스레드(deposit) 이 lock 을 전달 받아 deposit() 메서드를 실행시키고, notify() 메서드를 통해 대기 중인 스레드를 깨운다.
    1. wait() // 객체의 lock 을 풀고 스레드를 해당 객체의 waiting pool 에 넣는다.
    2. notify() // waiting pool 에서 대기 중인 스레드 중의 하나를 깨운다.
    3. notifyAll() // waiting pool 에서 대기 중인 모든 스레드를 깨운다.
  • wait() 과 notify() 의 활용
    1. remove() 메서드에서 dishes.size() == 0 이면, wait() 메서드를 만나서 lock을 반납함.
    2. dishes.size() != 0 면 음식을 소비하고, 요리사 스레드가 쉬고 있을 수 있으니 waiting pool에 notify() 함.
    3. 원하는 음식이 없는 경우, wait() 해서 lock 을 반납하고 waiting pool 로 이동해야 함
    4. add() 메서드에서 dishes.size() 가 테이블에 놓을 수 있는 MAX_FOOD 를 초과하면 요리사 스레드를 wait() 시킴.
  /**
  * 스레드 동기화 중 협력관계 처리작업 : wait() notify()
  * 스레드 간 협력 작업 강화
  */

  public synchronized void makeBread(){
      if (breadCount >= 10){
          try {
              System.out.println("빵 생산 초과");
              wait();    // Thread 를 waiting pool 로 이동 시킴
          } catch (Exception e) {

          }
      }
      breadCount++;    // 초과하지 않으면 빵 생산
      System.out.println("빵을 만듦. 총 " + breadCount + "개");
      notify();    // waiting pool 에 쉬고 있던 Thread(eatBread) 를 깨움
  }

  public synchronized void eatBread(){
      if (breadCount < 1){
          try {
              System.out.println("빵이 없어 기다림");
              wait(); // 0보다 작으면 Thread 를 waiting pool 로 이동시킴
          } catch (Exception e) {

          }
      }
      breadCount--;
      System.out.println("빵을 먹음. 총 " + breadCount + "개");
      notify();
  }

예상 질문

Q. 프로세스와 스레드의 기본적인 차이가 무엇인가요?
✅ 프로세스는 프로그램 코드, 메모리 및 할당된 리소스를 포함하는 실행 단위이며, 스레드는 동일한 메모리 공간과 리소스를 공유하는 프로세스 내에서 실행되는 가장 작은 실행 단위입니다. 프로세스 내에서 실제 작업을 수행하며, 모든 프로세스는 최소 하나의 스레드를 가지고 있습니다.

Q. 멀티 태스킹과 멀티 스레드의 차이점은 무엇인가요?
✅ 멀티태스킹은 여러 작업을 동시에 실행하는 것을 의미합니다. 다중 프로세싱에서는 각각 고유한 메모리 공간을 가진 여러 프로세스가 동시에 실행되지만, 멀티스레딩에서는 하나의 프로세스 내에서 여러 스레드가 실행되며, 동일한 메모리 공간을 공유합니다.

Q. 멀티 스레딩과 멀티 태스킹의 장단점은 무엇인가요?
✅ 멀티스레딩은 리소스를 효율적으로 활용하고, 응답성을 향상시키며, 코드 구조를 간소화하는 장점이 있습니다. 그러나 데드락과 같은 문제를 피하기 위해 동기화에 주의해야하며, 스레드 관리에서 복잡성이 대두될 수 있습니다.

Q. 자바에서 스레드는 어떻게 구현되며, Thread 클래스로 상속받거나 Runnable 인터페이스로 구현되는 차이점은 무엇인가요?
✅ 자바에서 스레드는 Thread 클래스를 확장하거나 Runnable 인터페이스를 구현함으로써 구현할 수 있습니다. Thread를 확장하는 것은 run() 메서드를 재정의해야 하지만, Runnable을 구현하는 것은 하위 클래스화가 필요하지 않으므로 관심사의 분리에 더 적합합니다.

Q. 스레드 실행 제어에서 interrupt() 메서드의 목적과 사용법을 설명해주세요.
✅ interrupt() 메서드는 대기 상태인 스레드의 상태를 대기에서 실행 가능한 상태로 변경하여 실행을 재개합니다. 스레드의 interrupted 상태를 true로 설정하며, isInterrupted() 또는 interrupted()를 사용하여 확인할 수 있는데, interrupted() 메서드는 현재 스레드의 interrupted 상태를 알려줌과 동시에 false 로 초기화시킵니다.

Q. suspend(), resume(), stop() 메서드를 사용할 때 주의할 점은 무엇인가요?
✅ 이러한 메서드들은 데드락 상황과 동기화 문제를 일으킬 가능성 때문에 사용이 중지되었습니다. 이러한 메서드들은 상태의 불일치와 예기치 않은 동작을 유발할 수 있으므로 사용을 지양해야 합니다.

Q. 동기화를 통해 공유 리소스에 대한 액세스를 관리하는 데 어떻게 도움이 되나요?
✅ 동기화는 하나의 스레드만이 한 번에 임계 구역에 액세스할 수 있도록 보장하여 공유 리소스의 동시 액세스를 방지하고 데이터 무결성을 유지합니다.

Q. 동기화에서 wait(), notify(), notifyAll() 메서드의 목적과 사용법을 설명해주세요.
✅ 이러한 메서드들은 스레드 간의 대기 및 알림 메커니즘을 관리하기 위해 사용됩니다. wait()는 잠금을 해제하고 스레드를 대기 상태로 전환하며, notify()는 대기 중인 스레드 중 하나를 깨우고, notifyAll()은 대기 중인 모든 스레드를 깨웁니다.

Q. 멀티스레드 애플리케이션에서 wait()과 notify()가 효과적으로 사용될 수 있는 시나리오를 설명해주세요.
✅ 이러한 메서드들은 생산자-소비자 시나리오에서 효과적으로 사용됩니다. 생산자 스레드는 버퍼에 공간이 생길 때까지 wait()를 사용하여 기다릴 수 있으며, 소비자 스레드는 데이터를 소비하고 버퍼에 공간이 생기면 notify()를 사용하여 생산자를 깨울 수 있습니다.

Q. 동기화가 멀티스레딩에서 왜 중요하며, 자바에서는 어떻게 동기화를 달성할 수 있나요?
✅ 동기화는 경쟁 조건을 방지하고 공유 리소스의 데이터 무결성을 유지하기 위해 멀티스레딩에서 중요합니다. 자바에서는 임계 구역의 코드를 지정하기 위해 synchronized 키워드를 사용하여 동기화를 달성할 수 있습니다.

참고자료
[Java] Thread
[자바의 정석 - 기초편] ch13-1 쓰레드 ~ [자바의 정석 - 기초편] ch13-34~36 wait()과 notify()

profile
주니어의 굴레는 언제 벗어날 것인가

0개의 댓글