ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [live-study] 10주차 :: 멀티쓰레드 프로그래밍
    JAVA/라이브 스터디 | whiteship 2021. 2. 15. 00:11

    https://youtu.be/HLnMuEZpDwU

    참고도서 : Java의 정석(3rd Edition)

     

     

     

    0. 프로세스와 쓰레드

    프로세스(Process)
    • 프로세스란 간단히 말해서 '실행 중인 프로그램' 이다.
    • 프로그램을 실행하게 되면 OS로부터 실행에 필요한 자원을 할당 받아서 프로세스가 된다.
    • 프로세스는 (프로그램을 수행하는데 필요한 데이터와 메모리 등의) 자원과 쓰레드로 구성되어 있다.
    쓰레드(Thread)
    • 즉, 쓰레드프로세스의 자원을 이용해서 실제로 작업을 수행하는 주체이다
    • 그래서 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재한다.
    • 이 때, 둘 이상의 쓰레드를 가지는 프로세스를 '멀티쓰레드 프로세스(multi-threaded process)'라고 한다.
    • 하나의 프로세스가 가질 수 있는 쓰레드의 개수는 제한되어 있지 않으나, 쓰레드가 작업을 수행하는데 개별적인 메모리 공간을 필요로 하기 때문에, 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다. 
    멀티태스킹(multi-tasking)과 멀티쓰레딩(multi-threading)
    • 멀티태스킹(multi-tasking, 다중작업)은 현재 우리가 사용하고 있는 대부분의 OS가 지원하고 있으며, 여러개의 프로세스가 동시에 실행될 수 있다.
    • 멀티쓰레딩(multi-threading)은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다. CPU의 코어가 한 번에 단 하나의 작업만 수행할 수 있으므로, 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.
    • 프로세스의 성능은 단순히 쓰레드의 개수에 비례하지는 않는다.
    멀티쓰레딩(multi-threading)의 장, 단점
    • 장점
      • CPU 사용률 향상
      • 자원 효율적 사용
      • 사용자 응답성 향상
      • 작업의 분리로 코드 간결
    • 단점 : 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에
      • 동기화(synchronization)
      • 교착상태(deadlock)

    1. Thread 클래스와 Runnable  인터페이스

    Thread의 구현
    • Thread를 구현하는 방법에는 Thread클래스를 상속받는 방법Runnable 인터페이스를 구현하는 방법 두 가지가 있다.
    • 두 가지 방법 모두 상관은 없지만, Thread 클래스를 상속받게 되면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적이다.
    Thread 클래스를 상속
    • Thread 클래스를 상속받아서 구현하는 방법은 다음과 같다.

    Runnable 인터페이스를 구현
    • Runnable 인터페이스를 구현하는 방법은 다음과 같고, 이 방법은 재사용성(reusability)이 높고 코드의 일관성(consistency)을 유지할 수 있기 때문에 보다 객체지향적인 방법으로 볼 수 있다.

    • 또한, Runnable 인터페이스는 run()만 정의되어 있는 간단한 인터페이스이다.

    • 즉, 쓰레드를 구현한다는 것은 Thread 클래스를 상속하는 방법이든 Runnable 인터페이스를 구현하는 방법이든, run()의 body{ } 부분을 채운 것이다.
    Thread 클래스 상속과 Runnable 인터페이스 구현 차이
    • Thread 클래스를 상속받은 경우와 Runnable 인터페이스를 구현한 경우의 인스턴스 생성 방법은 다르다.

    쓰레드의 실행 - start()
    • 위에서 본 것 처럼 쓰레드를 생성했다고 해서 자동으로 실행되는 것이 아니다. start()를 호출해야만 쓰레드가 실행된다.
    • 물론 실행순서는 OS의 스케쥴러가 작성한 스케쥴에 의해 결정된다.
    • 또한, 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다.
    • 하나의 쓰레드에 대해 start()가 한 번만 호출 될 수 있다는 뜻이다.
    • 만약 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 아래와 같이 IllegalThreadStateException이 발생할 것이다. 다시 호출하기 전에 쓰레드를 다시 생성해주어야한다.

    2. 쓰레드의 상태

    쓰레드의 상태
    • 쓰레드의 상태에는 아래와 같이 5가지가 있다.
    • JDK 1.5부터 쓰레드의 상태는 Thread의 getState() 메소드를 호출해서 확인할 수 있다.

    쓰레드의 스케쥴링과 관련된 메소드
    • 쓰레드의 스케쥴링과 관련된 메소드는 아래와 같다.
    • resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)으로 만들기 쉬워, derpecated 되었다.

    쓰레드의 생성부터 소멸까지
    • 쓰레드의 생성부터 소멸까지 과정에서의 쓰레드 상태는 아래와 같다.

    3. 쓰레드의 우선순위

    쓰레드의 우선순위 (priority)
    • 쓰레드는 우선순위(priority)라는 속성(멤버변수)를 가지고 있는데, 이 값에 따라서 쓰레드가 없는 실행시간이 달라진다.
    • 즉, 우선순위를 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.
    • 예를 들어, 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위를 다른 작업보다 높게 잡을 수 있다.
    쓰레드의 우선순위 지정
    • 쓰레드는 1 ~ 10의 범위내에서 우선순위를 가질 수 있으며, 숫자가 높을수록 우선순위가 높다.
    • 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속 받는다.
    • 또한, main메소드를 수행하는 쓰레드는 우선순위가 5이므로, main메소드내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.
    • 쓰레드의 우선순위와 관련된 상수와 메소드는 아래와 같다.

    4. Main 쓰레드

    run()이 아닌 start()
    • 앞서 봤을 때, 우리는 쓰레드를 실행시킬 때 run()이 아니라 start()를 호출하였다.
    • 왜 run()이 아니라 start()일까
    • main 메서드에서 run()을 호출한다는 것은 생성된 쓰레드를 실행시키는 것이 아니라, 단순히 클래스에 선언된 메소드를 호출하는 것 뿐이다.

    main메소드에서 run() 호출했을 때 호출스택

    • 반면 start()는 호출스택을 생성한다.
    • 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출한다.

    main쓰레드
    • main메소드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다.
    • 지금까지는 main메소드가 수행을 마치면 프로그래미이 종류되었으나, main메소드가 수행을 마쳤더라도 다른 쓰레드가 아직 작업 중이라면 프로그램이 종료되지 않는다. 
    • 즉, 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
    start()와 run()차이
    • 위에서 본 것처럼 start()를 호출하면 새로운 쓰레드를 만들게 된다. 
    • 한 쓰레드가 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.
    • 호출스택  그림에 main쓰레드가 없는 이유는 main쓰레다가 이미 종료되었기 때문이다.

    • run()을 호출하면 새로운 쓰레드를 만들지 않게 된다.
    • 아래 main쓰레드 호출스택에서 main메소드가 포함되어 있음을 확인 할 수 있다.

    5. 동기화

    동기화(Synchronization)
    • 싱글쓰레드와 달리 멀티쓰레드 프로세스 같은 경우에는 같은 프로세스 내의 자원을 여러 프로세스가 공유해서 작업하기 때문에, 서로의 작업에 영향을 주게 된다.
    • 그래서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하기 위해서, 임계 영역(critical section)과 잠금(lock)이라는 개념이 도입되었다.
    • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓는다. 그리고 단 하나의 쓰레드만이 공유 데이터(객체)가 가지고 있는 lock을 획득할 수 있고, lock이 있어야지만 그 영역 내의 코드를 수행할 수 있다.
    • 그리고 해당 쓰레드가 그 임계 영역을 벗어나서 lock을 반납하고, 또 다른 쓰레드가 lock을 획득하여야지만 임계 영역의 코드를 수행할 수 있다.
    • 위와 같이 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭할 수 없도록 막는 것을 '쓰레드의 동기화(synchronization)'이라고 한다.
    • 자바에서는 synchronizaed 블럭을 이용해서 쓰레드의 동기화를 지원했었다. 
    • JDK1.5부터는 java.util.concurrent.locks와 java.util.concurrent.atomic 패키지를 통해서 다양한 방식으로 동기화를 구현할 수 있도록 지원하고 있다.
    synchronized를 이용한 동기화
    • synchronized는 임계 영역을 설정하는데 사용되는 키워드이다.
    • 아래와 같이 두 가지 방식이 있다.

    • 첫 번째 방법은 메소드 앞에 synchronized를 붙이는 방법으로 메소드 전체가 임계 영역으로 설정된다. 쓰레드는 해당 메소드를 호출한 시점부터 해당 메소드가 포함된 객체의 lock을 얻어 작업을 수행하다가, 종료되면 lock을 반환한다.
    • 두 번째 방법은 메소드 내 코드의 일부분만 블럭{ }으로 감싸고, 블럭 앞에 synchronized를 붙이는 방법이다. 이 때의 참조변수는 lock을 걸고자 하는 객체를 참조해야한다. 이 블럭을 synchronized 블럭이라 하고, 블럭의 영역 안에 들어가면서 lock을 얻게 되고 마찬가지로 벗어나게 되면 lock을 반납한다.
    • 임계 영역은 멀티쓰레드 프로그램의 성능에 영향을 끼치기 때문에 메소드 전체보다는 synchronized 블럭으로 임계 영역을 최소화하는 것이 좋다.

    6.데드락

    데드락(Deadlock)
    • Deadlock(교착상태)란 2개 이상의 쓰레드가 자원을 점유한 상태에서, 서로 상태편의 자원을 사용하려고 기다리느라 진행이 멈춰 있는 상태를 말한다.
    • 예를 들어, Thread 1은 lock1을 가지고 있고, lock2를 기다리고 있다. 반대로 Thread 2는 lock2를 가지고 있고, lock1을 기다리고 있다. 이 때, 둘 다 상대방의 lock을 가질 수 없으며, 이렇게 차단된 상태로 유지된다.

     

    참고자료

     

    댓글

Programming Diary