[JAVA]ch12. 쓰레드(Thread)

황인권·2024년 6월 10일

JAVA

목록 보기
12/15

1. 프로세스와 쓰레드

  • 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

멀티 쓰레딩

  • 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.
  • CPU의 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 수행함으로써 여러 작업을 모두 동시에 수행하는 것처럼 보이게 한다.

멀티 쓰레딩의 장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

멀티 쓰레딩의 단점

  • 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화, 교착상태와 같은 문제들을 고려해서 신중히 프로그래밍해야 한다.

2. 쓰레드의 구현과 실행

  • 쓰레드를 구현하는 방법은 2가지가 있다.
  • Thread 클래스를 상속받으면 다른 클래스를 상속 받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다.
  1. Thread 클래스 상속
class MyThread_1 extends Tread {
	public void run() { /* 작업내용 */ }
}
  1. Runnable 인터페이스 구현
class MyThread_2 implements Runnable {
	public void run() { /* 작업내용 */ }
}
  • 인터페이스 생성
  1. Thread 클래스 상속
MyThread_1 t1 = new MyThread_1();
  1. Runnable 인터페이스 구현
Runnable r = new MyThread_2();
Thread t2 = new Thread(r);

Thread t2 = new Thread(new MyThread_2()); // 위의 두 줄을 한 줄로 간단히

쓰레드의 실행 - start()

  • start()를 호출하면 일단 실행대기 상태에 있다가 자신의 차례가 되어야 실행한다.
  • 하나의 쓰레드에 대해 start()가 한번만 호출될 수 있다.

3. start()와 run()

  • main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.
  • 반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.

main 쓰레드

  • main 메서드의 작업을 수행하는 쓰레드

    실행중인 사용자 쓰레드가 하나도 없을 떄 프로그램은 종료된다.


4. 싱글 쓰레드와 멀티 쓰레드

  • 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티 쓰레드보다 싱글 쓰레드로 프로그래밍 하는 것이 더 효율적이다.
  • 두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글 쓰레드 프로세스보다 멀티 프로세스가 더 효율적이다.

5. 쓰레드의 우선순위

  • 쓰레드는 우선순위(Priority)라는 속성(멤버변수)을 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.

6. 쓰레드 그룹

  • 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다.
  • 자바 어플리케이션이 실행되면, JVM은 main과 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다.

7. 데몬 쓰레드(Daemon Thread)

  • 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.
  • 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료된다.
  • 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.

데몬 쓰레드의 예

  • 가비지 컬렉터
  • 워드프로세서의 자동저장
  • 화면 자동갱신 ...

8. 쓰레드의 실행제어

쓰레드의 스케줄링과 관련된 메서드

쓰레드의 상태

쓰레드 상태 변화과정

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

번호의 순서대로 쓰레드가 항상 수행되는 것은 아니다!!


9. 쓰레드의 동기화

  • 멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 이러한 일을 방지하기 위해 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 방해받지 않도록 하는 '임계 영역'과 '잠금'이라는 개념이 필요하다.
  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다.
  • 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다.
  • 한 쓰레드가 실행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드의 동기화(Synchronization)'라고 한다.

Syncronized를 이용한 동기화

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

wait()과 notify()

  • 객체의 lock을 가진 상태로 오랜 시간 보내지 않도록 하기 위해 고안되었다.
  • 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출해 쓰레드가 lock을 반납하고 기다리게 한다.
  • 그러면, 다른 쓰레드가 lock을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다. 나중에 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 lock을 얻어 작업을 진행할 수 있게 한다.
  • wait()이 호출되면, 실행중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서의 통지를 기다린다.
  • notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지받는다.
  • notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만, 그래도 lock을 얻을 수 있는 것은 하나의 쓰레드일 뿐이고 나머지는 다시 기다리게 된다.
  • waiting pool은 객체마다 존재하므로 notifyAll()이 호출된 객체의 waiting pool에 대기중인 쓰레드만 해당된다.

기아현상과 경쟁상태

  • 기아현상
    • 어떤 쓰레드는 계속 통지를 받지 못하고 오랫동안 기다리게 되는 것
  • 경쟁상태
    • 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것

Lock과 Condition을 이용한 동기화

  • ReentrantLock
    • 가장 일반적인 lock이다. 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계 영역으로 들어와서 이후의 작업을 수행할 수 있다.
  • ReetrantReadWriteLock
    • 읽기를 위한 lock과 쓰기를 위한 lock을 제공한다.
    • 읽기 lock이 걸려 있어도 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있다.
      그러나, 읽기 상태에서는 쓰기 lock을 거는 것은 허용되지 않는다.
  • StampedLock
    • lock을 걸거나 해지할 때 '스탬프(long 타입의 변수값)'사용하며, 읽기와 쓰기를 위한 lockdhldp '낙관적 읽기 lock'이 추가된 것이다.
    • '낙관적 읽기 lockㅇ'은 쓰기 lock에 의해 바로 풀린다.
    • 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.
ReentrantLock lock = new ReentrantLock();

lock.lock();
//임계 영역
lock.unlock();

condition

  • 쓰레드의 종류에 따라 나누어 '경쟁상태'를 개선한다.
ReentrantLock lock = new ReentrantLock();

private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();

volatile

  • 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다.
  • 그러다보니 도중에 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아서 값이 달라지는 경우가 발생한다.
  • 이 때, volatie을 변수 앞에 붙이면 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어온다.
  • synchronized 블럭을 사용해도 블럭으로 들어가거나 나올 떄, 캐시와 메모리간의 동기화가 이루어지기 때문에 같은 효과를 얻을 수 있다.

long과 double을 원자화

  • JVM은 데이터를 4byte단위로 처리하기 때문에 8byte인 long과 double타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없다. 변수의 값을 읽는 과정에서 다른 쓰레드가 끼어들 여지가 있다.
  • 다른 쓰레드가 끼어들지 못하게 하려고 synchronized 블럭으로 감쌀 수도 있지만, 변수 선언할 때 volatile을 붙여도 된다.

fork & join 프레임웍

  • 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어 준다.
  • fork()
    • 해당 작업을 쓰레드 풀의 작업 큐에 넣는다. (비동기 메서드)
  • join()
    • 해당 작업의 수행이 끝날 때까지 기다렸다가, 수행이 끝나면 그 결과를 반환한다. (동기 메서드)
profile
inkwon Hwang

0개의 댓글