본문 바로가기

자바

멀티쓰레드 프로그래밍에서 필요한 지식들 - 스터디 8차

능동적으로 동작하는 쓰레드는 무엇이 있을까요?

프로세스와 쓰레드가 무엇인지? 자바에서는 이러한 개념들을 어떻게 다루는지 이야기 해볼게요.

추가로 Executors나 Future에 대해서도 이야기 하면 좋겠네요.

 

1.   프로세스와 쓰레드가 무엇일까?

자바에서 쓰레드를 다루기 전에 OS 단위에서 프로세스와 쓰레드가 무엇인지 사전 지식을 알 필요가 있습니다.

저는 이전에 프로세스와 쓰레드의 차이를 브라우져의 탭을 예를 들어 이야기 했었는데요.

하나의 브라우져가 열리는 것을 프로세스라고 하면 그 브라우져안에서 자원을 공유받아 사용하는 탭이 나뉘는게 쓰레드라고 설명하면서요. 한번은 누가 "정말 아는게 맞나요? 예시말고 제대로된 지식으로 말해 주세요."라고 한적이 있었는데 부끄럽게도 제대로 이야기 하지 못했던 기억이 있어요. ㅎㅎ 같이 깊에 알아보면 좋을 것 같네요.

 

위키백과를 찾아봤는데요.

프로세스는 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램을 말한다고 하네요. 프로그램의 상태가 메모리 상에서 실행되는 작업단위

 

 

 

이번에는 쓰레드, 쓰레드는 프로세스 내에서 실행되는 흐름의 단위

 

 

이거 무슨 소린가요? 제가 한글을 모르는 걸까요? 둘다 실행하는 단위라고 말하는데, 

 

그래서 제가 이해한 내용으로 정리해보겠습니다.

프로세스는 컴퓨터에서 실행중인 프로그램으로 CPU 스케쥴링의 대상이 되는 작업물

쓰레드프로세스라는 작업물에서 실제로 작업이 수행되는 것을 이야기 합니다. 비슷하게 보일 수 있는데 쓰레드는 프로세스에 포함되는거에요.

 

 

모든 프로세스는 실제 작업이 수행되기 때문에 하나의 쓰레드를 가지고 있고, 여러개의 쓰레드(작업)가 있으면 멀티 쓰레드라고 이야기 하는거죠. (프로세스 > 쓰레드)

 

좀 더 다양한 시선에서 본 것들

아니 그럼 스레드로 실제 수행하는 작업을 나누는 이유가 뭐야?

왜 프로세스가 있고 왜 쓰레드가 있는거야? 쓰레드가 자원을 굳이 공유한다고 하는데 이건 이유가 뭐야?

 

프로세스와 스레드는 운영체제의 개념입니다.

이들이 분리 되어 있는 이유는 목적과 자원 활용의 차이가 존재합니다.

 

프로세스는 실행 중인 컴퓨터 프로그램의 인스턴스입니다. 프로그램 코드와 현재 활동을 포함하고 있습니다. 각 프로세스는 별도의 메모리 주소 공간을 가지고 있어, 하나의 프로세스가 다른 프로세스의 메모리에 직접 접근할 수 없습니다. 이러한 격리는 프로세스가 다른 프로세스의 데이터에 실수로나 의도적으로 간섭할 수 없게 하여 시스템의 안정성과 보안을 향상시킵니다.

 

반면에, 스레드는 운영 시스템의 스케줄러가 독립적으로 스케줄링하고 관리할 수 있는 실행 가능한 명령어의 시퀀스입니다. 스레드는 자신만의 스택을 가지고 있지만, 부모 프로세스의 공유 데이터와 자원에 접근할 수 있습니다. 이 공유 접근은 동일 프로세스 내의 스레드들이 동시에 실행될 수 있게 하여 CPU 자원을 효율적으로 사용하게 합니다.

 

그래서 스레드로 분리하는 이유는? 프로세스는 독립된 자원을 사용하기 때문에 보안과 안정성을 보장하지만, 모두 각기 자원을 할당하기 때문에 낭비가 존재하고 이 자원을 효율적으로 사용하기 위해서 스레드로 동일 프로세스 내에서 데이터 공유와 효율적인 동시성 실행을 가능하게 하기 하는 것 이라고 볼수 있습니다.

 

컨텍스트 스위칭 (Context Switching)

컨텍스트 스위칭이라는 용어를 알면 좋을 것 같아요.

컨텍스트 스위칭은 PCB(프로세스 컨트롤 블록)를 교환하는 과정을 말하는데요.

CPU는 싱글 코어라고 가정했을때, 너무 빨라서 동시에 여러개를 실행하는 것처럼 보이지만 CPU는 한 순간에는 한개의 프로세스만 실행하고 있습니다. 계속 자원을 할당을 여기 줬다, 저기 줬다 이러한 작업을 하는 거에요.

 

이렇게 CPU 스케쥴링에 의해 프로세스에 할당된 시간이 끝나거나, 인터럽트가 발생하면 다른 프로세스로 자원을 할당하는 행위를 컨텍스트 스위칭이라고 말합니다. 생각해보니 쓰레드가 공유자원을 사용하면 분리하여 이러한 컨텍스트 스위칭 비용을 아낄수 있게 되는 장점도 있겠네요. 스레드는 스택영역을 제외한 모든 메모리를 공유하기 때문에 비용과 시간이 더 적게 걸리기 때문이죠.

 

https://www.geeksforgeeks.org/context-switch-in-operating-system/

 

Context Switching in Operating System - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

 

그러면 개념을 대충봤으니, 우리가 중요하게 알고 있어야 하는 것은 무엇일까요?
하나, 프로세스는 실제 작업을 수행하는 하나 이상의 쓰레드를 가지고 있는다.
둘, 쓰레드는 자원을 효율적으로 사용하기 위해서 프로세스의 자원을 공유해서 사용하게 한다.

 

그래서 결국 개발자가 쓰레드를 다룬다는 것은 쓰레드의 자원 공유의 동시성문제를 어떻게 컨트롤 할 것인가? 가 관심사가 되는거죠. 불변 객체로 만들어야한다. 쓰레드 세이프하게 코드를 작성해야 한다. 이런게 다 이러한 이유가 ㅎㅎ

 

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

사전 설명이 길었네요. 자바에서 쓰레드를 어떻게 사용하는지 같이 볼게요.

스레드(thread)를 이용하면 한 프로세스 내에서 두 가지 또는 그 이상의 일을 동시에 할 수 있습니다.

 

자바에서 스레드를 생성하고 사용하는 가장 기본적인 방법은 두 가지가 있습니다.

Thread 클래스를 직접 상속받아 사용하는 방법과 Runnable 인터페이스를 구현하는 방법입니다.

각각의 방법에 간단한 설명과 스레드를 사용하는 간단한 예시 코드를 작성 해볼게요.

 

Thread

Thread 클래스를 상속받아 run 메소드를 오버라이드하는 방법으로 스레드를 사용할 수 있습니다.

가장 기본적인 방법이며, 스레드 관련 메소드를 직접 사용할 수 있는 장점이 있습니다.

 

예시 코드

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread 실행: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 스레드 시작
    }
}

 

Runnable

Runnable 인터페이스를 구현하고, 그 인스턴스를 Thread 객체에 전달하여 스레드를 사용할 수 있습니다.

이 방법은 자바에서 다중 상속을 지원하지 않기 때문에, 이미 다른 클래스를 상속받고 있는 경우에 유용합니다.

 

예시 코드

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Runnable 실행: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 스레드 시작
    }
}

 

여러개의 인터페이스를 구현하는 구조의 예시 코드

interface AnotherInterface {
    void display();
}

class MyRunnable implements Runnable, AnotherInterface {
    // Runnable 인터페이스의 run 메소드 구현
    @Override
    public void run() {
        System.out.println("Runnable 실행: " + Thread.currentThread().getName());
    }
    
    // AnotherInterface 인터페이스의 display 메소드 구현
    @Override
    public void display() {
        System.out.println("AnotherInterface의 display 메소드 실행");
    }

    public class RunableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        
        // 고정된 스레드 풀 크기를 가진 ExecutorService 생성
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // MyRunnable 작업을 스레드 풀에 제출
        executor.submit(myRunnable);
        
        // 모든 작업이 완료되면 ExecutorService를 종료
        executor.shutdown();
    }
}

 

Runnable과 Thread의 선택

  • 확장성: Runnable을 사용하는 것이 일반적으로 더 유연하며 확장성이 높습니다. 다른 클래스를 상속받고 있는 경우에도 Runnable 인터페이스를 구현할 수 있기 때문입니다.
  • 리소스 공유: 같은 객체의 인스턴스 변수를 여러 스레드가 공유해야 하는 경우 Runnable을 사용하는 것이 더 쉽습니다.

두 방법 중에서 상황에 맞게 선택하여 사용하시면 될 것 같은데요. 실제 객체는 여러 상위 클래스를 상속받는 경우가 많기 때문에 저는 일반적으로 Runnable 인터페이스를 구현해서 Executors 사용하는 경우가 많은 것 같네요.

 

3.  쓰레드는 어떤 상태가 있을까?

자바에서 스레드의 상태는 java.lang.Thread.State에 enum으로 정의되어 있는데요.

말그대로 스레드의 객체의 라이프사이클에서의 상태를 정의하는 것을 말합니다.

친절하게 코드에 나와있는 java docs을 그대로 번역해보는 걸로 ㅎㅎ

 

NEW: 아직 시작되지 않은 새로운 스레드 상태입니다. start() 메소드가 호출되기 전의 스레드가 이 상태에 해당합니다.

 

RUNNABLE: start() 메소드가 호출되어, 실행 중이거나 실행 가능한 상태입니다. 스레드가 Java 가상 머신(JVM) 내에서 실행 중이지만, 운영 체제로부터 프로세서와 같은 추가 자원을 기다리고 있는 상태도 맞습니다.

 

BLOCKED: 모니터 락을 기다리며 차단된 상태입니다. 스레드가 동기화된 블록/메소드에 들어가기 위해 또는 Object.wait() 호출 후 동기화된 블록/메소드로 재진입하기 위해 모니터 락을 기다릴 때 이 상태에 있습니다.

 

WAITING: 특정 조건을 기다리는 상태입니다. Object.wait() (타임아웃 없음), Thread.join() (타임아웃 없음), LockSupport.park() 등을 호출하여 발생합니다. 스레드는 다른 스레드가 특정 작업을 수행하기를 기다립니다.

 

TIMED_WAITING: 지정된 시간 동안 기다리는 상태입니다. Thread.sleep(), Object.wait() (타임아웃 있음), Thread.join() (타임아웃 있음), LockSupport.parkNanos(), LockSupport.parkUntil() 등을 호출하여 발생합니다. 스레드는 지정된 시간이 지나거나 특정 조건이 만족되면 대기 상태에서 벗어납니다.

 

TERMINATED: 실행이 완료되어 종료된 스레드 상태입니다. run() 메소드의 실행이 끝나 스레드가 종료되었다는 것을 의미합니다.

 

좀 더 한눈에 볼 수 없는 그림이 없을까 싶어 블로그를 찾아봤어요.

더 자세한 내용으로 잘 정리되어 있는 글이니 참고하시면 좋을 것 같네요.

https://www.javabrahman.com/corejava/understanding-thread-life-cycle-thread-states-in-java-tutorial-with-examples/

 

Understanding Thread Life Cycle, Thread States in Java | Tutorial with Examples

Tutorial explains thread life cycle in Java with examples, Java threads lifecycle diagram, explains individual thread states, Java code example showing a thread moving through its states, logic of Java code example explained through a sequence diagram show

www.JavaBrahman.com

 

저는 이미지만 복사 ㅎㅎ

 

 

 

 

스레드의 상태를 보면서, 스레드가 프로그램 안에서 어떻게 동작하는지 이해하는 데 도움이 되시면 좋겠습니다.

 

4.  쓰레드도 우선순위를 설정할 수 있어.

 

5.  Main 쓰레드와 deamon 쓰레드

 

6.  동기화 (Synchronization) 에 대해서

스레드는 주로 필드와 참조 필드가 참조하는 객체에 대한 액세스를 공유하게 됩니다. 효율적이지만, 스레드 간섭과 메모리 일관성 오류라는 두 가지 유형의 오류를 발생시킬 수 있어요. 이러한 오류를 방지하기 위한 도구가 바로 동기화 (synchronization)입니다.

 

 

멀티스레딩은 프로세스 내 작업을 여러개의 스레드로 처리하는 기법입니다. 위에서 말씀드렸듯이 스레드끼리 서로 자원을 공유하기 때문에 효율성이 높습니다. 그러나 스레드는 메모리를 공유하기 때문에, 한 쓰레드가 사용하던 공유자원을 다른 쓰레드가 동시에 접근하는 문제가 생길 수 있다는 것을 항상 생각해야 해요.

 

이러한 둘 이상의 스레드가 공유자원에 접근할 때 순서들의 이유로 결과가 달라지는 코드영역을 임계영역이라고 하는데요.

Java는 synchronized 키워드를 사용하면 내부적으로 모니터 잠금 또는 내장 잠금이라고도 알려진 모니터를 사용하여 동기화 를 이용해서 동일한 객체의 모든 동기화된 블록에는 동시에 실행하는 스레드가 하나만 있을 수 있게하여 임계영역을 안전하게 보장 할 수 있습니다.

 

Synchronized 키워드 사용예시

public class Counter {

  private int count = 0;

  // 동기화된 인스턴스 메소드
  // 이 메소드는 한 번에 하나의 스레드만 접근할 수 있도록 동기화 됨
  public synchronized void increment() {
    count++;
  }

  // 동기화된 정적 메소드
  // 정적 메소드도 마찬가지로 동기화할 수 있으며, 이 경우 클래스 레벨에서 동기화 됨
  public static synchronized void staticIncrement() {
    // 여기에 정적 메소드의 구현이 들어갑니다.
  }

  // 동기화되지 않은 메소드
  // 이 메소드는 동기화되지 않으므로 여러 스레드가 동시에 접근 가능
  public int getCount() {
    return count;
  }
}

 

test 코드

public class CounterTest {

  @Test
  public void testIncrement() throws InterruptedException {
  
  	// 공유 자원인 Counter객체
    final Counter counter = new Counter();

    // 스레드를 생성하고 시작하는 루프 (10개의 스레드를 생성)
    Thread[] threads = new Thread[10];
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
          // 각 스레드는 1000번씩 카운터를 증가시킴
          counter.increment();
        }
      });
      threads[i].start();
    }

    // 모든 스레드가 종료될 때까지 기다람
    for (Thread thread : threads) {
      thread.join();
    }

    System.out.println("counter.getCount() = " + counter.getCount());
    // 최종 카운트 값이 예상과 일치하는지 확인
    Assertions.assertEquals(10000,
                            counter.getCount(),
                            "모든 스레드가 각각 1000배씩 증가한 후 10000이어야 합니다.");
  }
}

 

위 코드를 돌릴때, increment 메서드에 syncronized 키워드를 제거하면 테스트에 실패하는 것을 볼 수 있습니다.

공유자원인 Counter 객체의 count에 동기화 작업이 이루어져 있지 않기 때문이에요.

 

좀 더 알면 좋은 지식들

Concurrent와 Parallel

알아야할 지식 하나 추가할게요. Concurrent와 Parallel입니다.

 

Concurrent(병행)  Parallel(병렬) 프로그래밍은 여러 작업을 동시에 처리하는 두 가지 접근 방식인데요. 이 두 용어는 비슷하지만 다른 명확히 구분할 필요가 있습니다.

 

Concurrent (병행)

병행 프로그래밍에서는 여러 작업이 동시에 진행되는 것처럼 보이지만, 실제로는 하나의 프로세서가 여러 작업을 번갈아 가며 처리합니다. 이는 작업 간의 스위칭이 매우 빠르게 이루어져서 동시에 처리되는 것처럼 보이게 하는 기술입니다. 병행성은 멀티태스킹을 가능하게 하는 기본 원리로, 하나의 CPU 코어에서도 여러 프로세스나 스레드가 동시에 실행될 수 있도록 합니다.

 

Parallel (병렬)

병렬 프로그래밍은 여러 CPU를 활용하여 동시에 여러 작업을 동시에 진행합니다. 이 접근 방식에서는 성능 향상을 위해 작업을 여러 코어에 분산시키는 것이 핵심입니다. 병렬 처리는 데이터 처리, 대규모 계산, 과학적 모델링 등과 같이 고성능이 요구되는 작업에 사용 됩니다.

 

7.  데드락

 

 

Reference

[쓰레드와 프로세스의 차이]

https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4

https://ko.wikipedia.org/wiki/%EC%8A%A4%EB%A0%88%EB%93%9C_(%EC%BB%B4%ED%93%A8%ED%8C%85)

https://www.geeksforgeeks.org/difference-between-process-and-thread/

 

[자바 Thread & Runnable]

https://wikidocs.net/230

https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html

https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html

 

[쓰레드의 상태]

https://www.javatpoint.com/thread-states-in-java

https://www.javabrahman.com/corejava/understanding-thread-life-cycle-thread-states-in-java-tutorial-with-examples/

https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html

 

[동기화 synchronized]

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html

https://www.baeldung.com/java-synchronized