Language/Java

Multithreading in Java

Kim Jinung 2023. 5. 17. 15:47

Multithreading

프로세스에서 스레드 여러 개를 동시에 실행하는 기법이다. 

Thread

프로세싱의 가장 작은 유닛 단위로, 가벼운 서브 프로세스에 해당한다. 멀티쓰레딩과 멀티프로세싱 모두 멀티태스킹을 위해 사용한다. 보통 멀티쓰레딩을 멀티프로세싱보다 더 많이 사용한다. 멀티쓰레딩은 공유 메모리 영역을 사용하기 때문에 별도의 메모리 영역을 분리해서 사용하지 않는다. 따라서 컨텍스트 스위칭(Context Switching) 과정에서 멀티프로세싱 보다 더 적은 시간을 필요로 한다. (오버 헤드가 적다.)

 

JVM의 메모리 영역에는 Heap과 Thread stack이 존재한다. Heap은 쓰레드들이 공유하는 메모리 영역이다. Thread stack은 개별 쓰레드마다 할당 받고 프레임을 생성해서 지역 변수 등을 저장하는 공간이다.


Life Cycle of a Thread

https://www.javatpoint.com/life-cycle-of-a-thread

쓰레드는 항상 다음과 같은 상태 중 하나에 반드시 해당한다.

 

  1. New: 쓰레드가 새로 생성된 상태다. 아직 start 메서드가 호출되지 않은 상태다.
  2. Active: Runable, Running에 해당하는 상태다.
    1. Runable: 쓰레드가 실행될 수 있는 상태다. 멀티쓰레드 환경에서 쓰레드는 일정 시간 동안 CPU를 사용하고, 시간이 초과되면 자발적으로 CPU 제어권을 다른 쓰레드에게 양도한다.
    2. Running: 쓰레드가 CPU를 할당 받아 실행하고 있는 상태다. (즉 Runable -> Running -> Runable 상태를 반복하게 된다.)
  3. Blocked or Waiting: 쓰레드 A가 프린터에게 요청을 하려는데, 쓰레드 B가 이미 사용 중인 경우 쓰레드 A는 쓰레드 스케쥴러가 쓰레드 A를 활성화하기 전까지 대기 상태에 진입하게 된다.
    1. Blocked: 쓰레드 B는 크리티컬 섹션에 진입하기 위해서 Lock을 획득한다. 이때 쓰레드 A가 동일한 크리티컬 섹션에 진입해서 변수를 조작하고자 할 때 Lock을 이미 쓰레드 B가 Lock을 가지고 있으므로 A는 Lock을 획득하기 전까지 잠시 실행을 중지한다. 즉 특정한 자원을 얻기 위해 대기하는 상태다.
    2. Waiting: 쓰레드가 다른 쓰레드가 호출하기 전까지 대기하는 상태다. 주로 쓰레드 간 동기화를 위해 사용한다. 쓰레드는 wait 메서드로 대기 상태에 들어갈 수 있다.
  4. Timed Waiting: 쓰레드 제한된 시간만큼만 대기하도록 만든다. 기아 현상을 방지하기 위해 존재한다. Sleep 메서드가 그 예시 중 하나다.
  5. Terminated: 쓰레드가 잡을 정상 종료하거나, 비정상 종료될 때 상태다.

Multithreading example

public class ThreadSample implements Runnable{
    private String threadName;

    public ThreadSample(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        System.out.printf("[%s] Start thread%n", threadName);
    }
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("[Main Thread] Execute main thread");

        Thread thread1 = new Thread(new ThreadSample("Thread1"));
        Thread thread2 = new Thread(new ThreadSample("Thread2"));

        thread1.start();

        thread2.start();

        try {
            thread1.join();

            thread2.join();
            
            System.out.println("[Main thread] All sub threads have completed");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("[Main Thread] End Process");
    }
}
  1. ThreadSample 클래스는 Runnable 인터페이스를 구현하기 위해서 run 메서드를 오버라이드한다.
  2. Thread 클래스를 이용해서 쓰레드 인스턴스를 생성한다.
  3. Thread 인스턴스는 start 메서드를 이용해서 시작할 수 있다.
  4. Thread 인스턴스의 join 메서드를 사용해서 메인 쓰레드에 서브 쓰레드를 참여 시킨다.

위 코드를 실행한 결과는 다음과 같다.

  1. 메인 쓰레드 실행
  2. 쓰레드1 실행
  3. 쓰레드2 실행
  4. 쓰레드1이 메인 쓰레드에 조인
  5. 쓰레드2가 메인 쓰레드에 조인
  6. 메인 쓰레드는 서브 쓰레드가 모두 종료되면 이를 콘솔에 출력
  7. 프로세스 종료

 

위 과정에서 두 가지 포인트가 있다.

  1. 쓰레드로 생성한 객체의 실행은 start 메서드가 담당한다. 이때 쓰레드의 상태는 New -> Runnable이 된다. 오버라이드 한 Run 메서드에는 Running 상태에서 해당 쓰레드가 수행할 작업이 정의했다. (유저 입장에서 run을 호출하면 안되는 이유다. 유저는 start 메서드로 해당 메서드가 runable한 상태라는 것만 알려준다. run은 cpu를 쥐어줬을 떄 실행해하니까 강제로 실행시키면 안 되는 것)
  2. 서브 쓰레드의 join 메서드로 메인 쓰레드에 참여 시켰다. 메인 쓰레드가 서브 쓰레드를 호출하는 주체다. join 메서드는 쓰레드를 실행 시킨 메인 쓰레드에 참여 시키는 메서드다. 따라서 메인 쓰레드는 서브 쓰레드가 완전히 종료되기 전까지 종료되지 않는다. (만약 join 메서드를 호출하지 않는 경우 메인 쓰레드는 할 일을 마치고 먼저 종료되는데, 로컬에서 코드를 돌려보니까 시간차를 두고 실행해도 서브 스레드가 정상 수행된다. 이건 조금 더 찾아봐야 할듯) 

Synchronization in Java

멀티 쓰레드가 공유 변수에 접근할 때 쓰레드 하나만 접근할 수 있는 기능을 제공한다.

Types of Synchronization

  1. Mutual Exclusive
  2. Cooperation(Inter-thread communication)

Mutual Exclusive(상호 배제, 독점)

Mutual exclusive의 특징은 특정 쓰레드가 공유 변수에 대한 Lock(Monitor)를 쥐고 작업을 하고 있으면,  다른 쓰레드는 공유 변수에 접근을 못하게 하는 방법이다.

  • Synchronization method: 메서드 키워드에 포함 시키는 방법
  • Synchronization Block: 코드 블록 내에서 동기화를 정의
  • Static Synchronization: 정적 메서드를 위한 동기화

Cooperation(Inter-thread communitation)

동기화 쓰레드가 상호 소통할 수 있는 방법을 제공한다. 다음과 같은 메서드가 존재한다.

 

  • wait : 현재 스레드가 공유 변수에 대한 Lock을 풀도록 명령한다. 그리고 다른 쓰레드가 작업을 완료하고 notify 혹은 notifyAll를 메서드를 호출하기 전까지 대기한다. 이를 위해서 대기하는 쓰레드는 객체의 모니터를 소유하고 있는다. 
  • notify: 객체의 모니터(권한)을 기다리고 있는 단일 쓰레드에게 Lock이 풀렸음을 알린다.
  • notify: 객체의 모니터(권한)을 기다리고 있는 모든 쓰레드에게 Lock이 풀렸음을 알린다.

해당 메서드들은 동기화된 메서드들 간에서만 실행해야 한다. 그러지 않으면 예외가 발생할 수 있다. 

*Thread 클래스의 sleep 은 lock을 릴리즈 하지 않는데, Object 클래스의 wait은 lock을 릴리즈 한다.

Reentrant Monitor

쓰레드는 동일한 모니터를 재사용할 수 있는 기능이다. 이 말이 굉장히 헷갈리는데, 만약 동기화 메서드 내부에서 같은 공유 변수를 사용하는 동기화 메서드를 사용하는 경우 모니터(Lock)을 재사용할 수 있다는 것이다.

 

만약 Reentrant Monitor 기능이 제공되지 않는 경우에서 동일한 공유 변수를 사용하는 두 메서드 A, B가 존재하고 A 내부에서 B를 호출하면 데드락이 발생한다. 이미 A 메서드가 모니터를 가지고 있고, B를 실행하기 위해서는 공유 변수의 모니터를 쥐고 있어야 하는데, 논리적으로 현재 메서드가 쥐고 있기 때문이다.

 

처음에는 락을 릴리즈하고 재획득이라고 생각했는데, 내부 메커니즘은 그대로 재사용이라고 한다. 다시 요약하자면 동기화 메서드 내부에서 같은 공유변수를 사용하는 동기화 메서드를 호출하면 모니터(Lock)을 재사용할 수 있다. 보통 멀티 쓰레드 환경에서 재귀를 구현할 때 많이 사용된다고 한다.


Summary

멀티 쓰레드를 사용하면 Critical section 문제에 직면할 수 있다. 이를 위해서 자바는 Sychronized 키워드를 제공한다. 그리고 크게 두 가지 방법이 존재한다. 상호 배제(Mutual exclusive), 쓰레드간 통신(Inter-thread communication)이다. 그런데 이 두 방법이 독립적인 개념이 아니라 조합해서도 사용할 수 있다.

 

큰 특징으로는, 상호 배제 방법은 쓰레드가 공유 변수에 대한 Lock을 얻고, 작업을 완료해서 해당 공유 변수에 대한 Lock을 릴리즈 하면, 해당 공유 변수의 Lock을 얻기 위해서 대기 중인 다음 쓰레드가 작업을 이어나가는 흐름이다. 다음으로 쓰레드 간 통신은 쓰레드끼리 wait, nofity 메서드 등을 사용해서 상호 간 통신하는 방법이다. 쓰레드 간 통신이 설계 측면에서 더욱 복잡하다. 공유 변수에 대한 제어와 데드락 방지를 위한 가장 간단한 방법은 상호 배제 방법이라고 생각한다.

 


Reference

https://www.javatpoint.com/multithreading-in-java

 

Multithreading in Java - javatpoint

Multithreading in Java. Concurrently executing multiple threads apparently at the same time is known as multithreading. Let's see the explanation and usage of java multithreading with example.

www.javatpoint.com