카테고리 없음

[Java] 자바 쓰레드와 동기화

우디혜 2021. 3. 1. 23:06

프로세스

실행 중인 프로그램(program)

  • 프로그램 실행 → OS에서 프로그램 코드를 메모리에 적재(할당) → 프로세스 생성

쓰레드

  • thread is a abstraction of execution in a program
  • 프로그램 카운터와 실행 컨텍스트 일부를 추상화
  • 실행 흐름에 대한 추상화
  • 메소드(함수)가 최소단위
자바 실행 시 메인쓰레드 이외의 쓰레드도 동작한다! 즉, 이미 멀티 쓰레드로 작동하고 있다. 싱글 쓰레드로 작동하는 것 처럼 보일 뿐.

메인 쓰레드 생성 이후 JVM 작동을 위해서 자동으로 GC 등의 작업을 담당하는 스레드가 생성된다. 때문에 메인 쓰레드의 스레드 넘버가 1이라고 가정했을 때, 메인 안에서 처음으로 new Thread()로 새로운 유저 쓰레드를 생성한 유저 쓰레드의 스레드 넘버는 2가 아니다. 실습에서는 18번이었다. (JVM 스레드 생성 이후 만들어지기 때문에)

멀티쓰레드 프로세스

둘 이상의 스레드를 가진 프로세스

(+)

  • CPU 사용률을 향상
  • 자원을 효율적으로 사용함 → 프로세스 내의 자원을 여러 스레드들이 공유
  • 응답성 향상 → 백그라운드 작업을 하는 와중에도 프론트에서 다른 작업을 동시에 할 수 있다.

(-)

  • 자원을 공유하기 때문에 스레드 간의 동기화(synchronization), 교착상태(deadlock)와 같은 문제가 발생할 수 있다.

쓰레드 구현

스레드를 구현하는데는 2가지 방법이 있다.

 

1. Thread 상속

  • extends Thread, run() 오버라이딩
  • 다른 클래스를 상속받을 수 없다.
CustomThread t1 = new CustomThread();

 

2. Runnable 인터페이스 구현

  • implements Runnable, run() 구현
  • 재사용성 높다. 일관성 유지하기 쉽다(인터페이스이므로) → 객체지향적인 방식
  • 구현한 runnable을 thread 생성자의 매개변수로 넘겨주어야 한다.
Runnable r = new CustomThread(); 
Thread t1 = new Thread(r); 
// 혹은 
Thread t2 = new Thread(new CustomThread());

start()와 run()

start()를 호출하면 스레드가 실행대기상태가 된다. 이때 새로운 쓰레드가 생성되고 새로운 쓰레드에 대한 call stack이 생성되는 등 쓰레드가 작업에 필요한 환경이 만들어진다.

그리고 내부적으로 run()을 호출하면서 생성된 쓰레드의 call stack 맨 아래 run()이 담긴다.

(메인 쓰레드 : 새로 생성된 쓰레드 = main() : run() 이런 식으로 생각했다)

run()만을 호출하게 되면 call stack이 생성되는 것이 아니라 단순히 메인에서 run() 메소드가 실행된다.

프로그램 종료 시점

자바에서는 실행 중인 사용자 쓰레드(user thread)가 하나도 없을 때 프로그램이 종료된다. 때문에 main이 리턴되고서도 프로그램이 여전히 죽지 않을 수도 있다. 예를 들어 메인쓰레드에서 일반 쓰레드 만들고 Thread.sleep(10) 걸어주면 메인 쓰레드는 다 끝났지만 일반쓰레드는 아직 실행상태다. 다시 말해 메인 쓰레드가 죽는다고 프로그램이 죽는건 아니다!

데몬 쓰레드 (daemon thread)

일반 쓰레드( 데몬 스레드가 아닌 스레드)가 모두 종료되면 강제로 자동 종료된다.

  • 가비지 컬렉터
  • 워드프로세스 자동저장

쓰레드 상태

new

  • Thread 객체가 만들어진 상태
  • not yet started, not runnable as well
  • not known to thread scheduler

runnable

  • Thread 객체가 start() 호출한 뒤
  • 실행 중 혹은 실행 가능한 상태
  • can be scheduled

blocked

  • Runnable → Unrunnable
  • lock 메소드(sleep()과 같이)로 스케줄링 될 수 없는 상태
  • cannot be scheduled

dead

  • run method complete
  • cannot be rescheduled

스레드 상태

스레드 실행 제어

sleep()

지정된 시간 만큼 스레드가 일시정지된다.

// sleep() 메소드가 static인 이유 
// 반드시 현재 실행되고 있는 스레드가 sleep() 상태가 될 것이기 때문 
Thread.sleep(1000);

join()

다른 쓰레드의 작업이 끝날 때까지 기다린다. 만약 join() 메소드에 매개변수로 시간을 설정해주게되면 지정된 시간 만큼 다른 스레드를 기다린다.

public static void main(){ 
	Thread t = new Thread(); 
	t.join(); // main 쓰레드가 t쓰레드의 작업이 끝날 때까지 기다린다.
}

 

interrupt()

1. sleep 중인 thread를 인터럽트

  • InterruptedException 발생 → try- catch로 반드시 핸들링 해줘야 함
  • 실행 대기상태가 됨

2. 진행 중인 쓰레드의 작업이 끝나기 전에 인터럽트

  • 현재 하고 있는 작업을 멈추라고 요청
  • 쓰레드의 작업 자체를 종료시키는 것은 아님
t.interrupted(); // interrupted 상태 확인 후 interrupted 상태를 다시 false로 변경 
t.isInterrupted() // 현재 스레드의 interrupted 상태만 확인

 

yield()

하던 일 중단하고 다른 스레드에게 CPU 점유를 양보한다. yield()를 호출한 스레드는 다시 실행 대기 상태가 된다. (yield()를 호출했다고 하던 일을 영원히 못하는건 아니다.)

동기화(Synchronization)

멀티 쓰레드 환경에서는 공유 자원에 대한 접근 문제가 발생할 수 있다(deadlock, race condition 등). 이때 데이터의 무결성이 보장되어야 하는 임계영역(critical section)을 설정해서 한 스레드 혹은 지정된 숫자의 스레드가 작업을 진행하는 동안 다른 스레드가 간섭하지 못하도록 막는 것을 동기화라고 한다.

Race Condition

  • 비행기 티켓을 예약하려고 할 때, clientA와 clientB가 거의 동시에 같은 좌석을 선택하고 결제를 진행했다. 원래라면 clientA가 좌석을 선택하고 나면 clientB의 예약화면에도 반영이 되어야하지만, '좌석을 선택하고 결제하는 시간대'가 겹쳐버리게 되면서 결과적으로 clientA가 먼저 좌석을 선택하고 결제했음에도 불구하고 clientB가 마지막에 결제한 내역이 최종적으로 반영되는 이상한 상황이 발생하게 될 수도 있다.
  • 때문에 '좌석을 선택하고 결제하는 과정'에 한해서는 한 명의 클라이언트만 진행할 수 있도록 해야한다. 그렇게되면 먼저 좌석을 선택한 clientA가 좌석을 선택하고 결제까지 진행하는 동안 clientB가 동일한 좌석에 대해 결제를 진행하는 일이 벌어지지 않을 것이다.

동기화 문제 해결 - synchronized 키워드 활용

synchronized 키워드는 크게 두 가지 영역에서 사용이 가능하다.

  1. synchronized 메서드
    • 메소드 종료 시 쓰레드는 lock을 반환
  2. synchronized 블럭
    • 블럭 종료 시 쓰레드는 lock을 반환

synchronized는 객체에만

  • synchronized 키워드는 자기자신에만 걸어줄 수 있다. 예를 들어 아래와 같이 static 변수에는 걸어줄 수 없다.
public static long result = 0; 
synchronized(result){ // 컴파일 에러 발생 -> result가 object가 아니기 때문에 에러 발생 
	result += i; 
}

동기화 오버헤드

  • lock 닫고 푸는데 시간이 오래걸리기 때문에 이로 인해 성능이 저하될 수 있다. 멀티 쓰레드의 장점을 제대로 살리지 못하는 경우가 생길 수 있다.
  • 동기화 오버헤드 관련 예제를 아직 정확하게 이해하지 못했다. 조금 더 공부해봐야겠다.

효율적인 동기화 처리 - wait()과 notify()

동기화된 임계영역의 코드를 수행하다가 작업을 진행할 상황(데드락과 같은 상황)이 아니게되면 wait()을 호출하여 해당 쓰레드가 lock()을 반납하고 waiting pool에서 다시 작업이 진행될 상황이 오기까지 대기한다.

 

  • Object 클래스에 정의되어있다. 특정 객체에 종속적인 동작이기 때문이다.

  • 동기화 블록 내에서만 사용할 수 있다.

  • 조금 더 공부를 해야할 부분