Thread에 대하여

띠로리·2022년 8월 24일

Thread 란?

쓰레드는 프로세스 내에서 독립적으로 실행되는 하나의 작업 단위이다.
일반적으로 하나의 프로세스는 한 가지 일을 하는데, 소프트웨어가 발달하면서 하나의 프로그램에 더 복잡한 동시 작업이 요구되었고, 그로 인해 쓰레드라는 더 작은 개념이 만들어졌다.

프로세스 간에는 서로 완벽히 독립된 공간을 가지지만, 쓰레드는 스택을 제외한 메모리 영역을 공유하는 특징이 있다.


사용 방법

자바에서 쓰레드를 생성하는 방법은 두 가지가 있다.
Runnable 인터페이스를 사용하는 방법과 Runnable 인터페이스를 구현한 구현체 Thread 클래스를 사용하는 것이다.

1. Thread 클래스를 통한 쓰레드 생성

Thread 클래스를 상속받은 클래스에서는 이 run() 메서드를 오버라이딩하여 사용해야 한다.
Thread가 시작된다면 가장 먼저 run() 메서드가 실행되게 된다.

이후 객체를 만들어 start() 메서드를 호출하면, test 쓰레드는 Runnable한 상태가 되고, 후에 JVM에 의해 test 쓰레드가 실행되게 된다면 run() 메서드를 호출하고 실행한다.

start() 메서드는 Thread 클래스에 의해 구현된 메서드이며, 생성된 쓰레드 객체를 Runnable 한 상태로 전환하는 메서드이다.
따라서 start() 메서드를 오버라이딩하면 안된다.

2. Runnable 인터페이스를 통한 쓰레드 생성

Runnable 인터페이스를 구현해 만든 구현체에는 start() 메서드가 존재하지 않기 때문에 별도의 Thread 객체를 생성하고 구현체를 인자로 넘겨주어 쓰레드 객체를 Runnable 한 상태로 만들어줘야 한다.


쓰레드의 상태

쓰레드는 총 6가지 상태를 가지며 이 상태들은 JVM에 의해 관리된다.
그림과 설명을 통해 알아보자.

1. New

new Thread() 를 통해 쓰레드를 생성하면 쓰레드는 New 상태가 된다.
이후 start() 메서드가 호출되면 쓰레드는 실행시킬 수 있는 Runnable 한 상태가 된다.
(이 때 여러 개의 쓰레드가 Runnable 한 상태이면 JVM이 우선 순위가 높은 쓰레드를 먼저 선택해 실행한다.)

2. Runnable

쓰레드가 준비 상태에 있거나 실행되고 있는 상태 모두를 포함한다.

3. Time-waiting

실행되고 있던 쓰레드에서 sleep(long x) 를 호출하면 x 밀리초만큼 이 상태가 되고, 이후 다시 Runnable 한 상태로 돌아가게 된다.

4. Block

쓰레드가 실행 도중 I/O 작업을 실행하면 JVM이 해당 쓰레드를 멈추고 다른 쓰레드를 스케줄링하고 해당 쓰레드는 block 상태가 된다. I/O 작업이 완료되면 다시 Runnable 한 상태가 된다.

컴퓨터 연산 작업에 비해 I/O 작업은 매우 느리기 때문에 I/O 작업 수행 완료 시까지 CPU가 대기 상태가 된다는 것은 매우 비효율적이다. 따라서 해당 쓰레드를 I/O 작업이 완료될 때까지 block 상태로 두고 컴퓨터는 다른 쓰레드를 스케줄링 하는 것이다.

5. Wating

쓰레드가 어떤 객체에 대해 wait() 메서드를 호출했을 때 발생된다.
이 경우 다른 쓰레드 객체에서 notify(), notifyAll() 등을 호출해 줄 때까지 대기하게 된다. (밑의 코드에서 살펴보자.)

6. Terminated

쓰레드가 종료된 상태로, 한 번 이 상태로 돌입한 쓰레드는 다시 다른 상태가 될 수 없다.


쓰레드의 우선순위

JVM의 쓰레드에 대한 스케줄링은 우선순위에 따라 이루어진다.
우선순위가 높은 쓰레드일 수록 더 많은 작업시간을 할당받으며 쓰레드별로 우선순위를 부여할 수도 있다.
또한 이 우선순위는 상대적인 것이며, 쓰레드 클래스 내부에 멤버변수로 존재한다.

  • 우선순위 체계는 1~10 으로 나뉘어져 있다.
  • 자식 쓰레드는 부모 쓰레드의 우선순위를 물려받는다.
  • 쓰레드의 setPriority(int x) 메서드를 사용하면 우선순위를 x로 바꿀 수 있다.

메인 쓰레드

JVM은 사실 자바 어플리케이션을 실행하기 직전 하나의 쓰레드를 만드는데, 이 쓰레드가 메인 쓰레드가 되어 main() 메서드를 실행하게 된다.
또한 기본적으로 Garbage Collection 쓰레드는 자동 생성되기 때문에 기본적으로 2개의 쓰레드가 돌아간다고 보면 된다.


동기화 (Synchronized)

멀티 쓰레드 프로세스의 경우 프로세스 내 자원이 여러 쓰레드에 공유될 수 있다.
이렇게 될 경우, 예상치 못한 데이터변경으로 인해 작업 결과값이 의도치 않게 달라질 수 있다.
따라서 쓰레드간의 작업을 간섭하지 못하도록 막아줄 필요가 있는데 이것을 동기화(Synchronized) 라고 한다.
(자바에서는 synchronized 키워드를 사용한다.)

synchronized 키워드 알아보기

synchronized 키워드는 특정 객체나 메서드에 lock을 걸 수 있다.
이 키워드를 사용하면 해당 블럭이 '임계 영역' 으로 지정되어 다른 쓰레드가 간섭할 수 없는 상태가 된다.

메서드에 lock 거는 방법

위 그림과 같이 메서드 선언부에 'synchronized' 키워드를 붙여주면 해당 메서드가 실행되는 동안 메서드를 가지고 있는 객체에 lock이 걸린다.
즉, synchronized 키워드가 붙어있는 메서드가 실행되는 동안 다른 쓰레드가 메서드의 this 객체에 영향을 끼칠 수 없다.

또한 위 코드와 같이 synchronized(객체의 참조변수)를 사용하면 메서드가 실행되는 동안 참조한 객체에 lock이 걸리게 된다.


데드락 (DeadLock)

두 개 이상의 쓰레드들이 서로 원하는 자원을 얻지 못해서 무한히 자원을 기다리는 상태를 말한다.

lockAB() 메서드를 보면 synchronized(A) 를 통해 A 객체에 lock을 건 상태에서 synchronized(B)를 호출해 B에 대한 lock도 원하고 있다. 그러나 lockBA() 메서드를 호출하는 쓰레드가 이미 B에 대한 lock을 걸고 있기 때문에 교착상태에 빠질 수밖에 없다.

이렇게 데드락이 발생하게 된다면 어플리케이션을 종료하는 것 말고는 해결할 수 있는 방법이 없다.
때문에 synchronized 대신 Lock 클래스의 tryLock을 사용해 lock 의 시간을 제한하거나, 모든 쓰레드에 필요한 lock의 순서를 통일해서 데드락이 걸리지 않도록 코드를 짜야한다.

profile
꼼퓨타

0개의 댓글