새발블로그

[JAVA] 멀티스레드 본문

Language/Java

[JAVA] 멀티스레드

EUG 2025. 5. 4. 21:14

1. 스레드(Thread)란?

  • 스레드는 프로그램 내에서 독립적으로 실행되는 흐름의 단위.
  • 하나의 프로세스는 최소한 하나 이상의 스레드를 가질 수 있다.
  • 프로세스: 프로그램이 메모리에서 실행되는 단위
  • 스레드: 프로세스 내에서 실행되는 작업의 흐름

2. 스레드 생성 방법

1) Thread 클래스 상속

Thread 클래스를 상속받아 run() 메서드를 오버라이드하고, start() 메서드를 호출하여 스레드를 시작한다.

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

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

2) Runnable 인터페이스 구현

Runnable 인터페이스를 구현하여 run() 메서드를 정의한 후, 이를 Thread 객체에 전달하여 실행할 수 있다.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running");
    }
}

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

3) 익명 클래스 사용

Runnable 인터페이스나 Thread 클래스를 익명 클래스 형태로 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread is running using anonymous class");
            }
        });
        thread.start();
    }
}

 

3. 스레드 동기화(Synchronization)

여러 스레드가 공유 자원을 동시에 접근할 때 문제가 발생할 수 있다. 이를 해결하기 위해 동기화를 사용하여 한 번에 하나의 스레드만 자원에 접근할 수 있도록 한다.

1) synchronized 키워드 사용

sychronized 키워드를 사용하면 메서드나 블록을 동기화하여, 한 번에 하나의 스레드만 해당 코드에 접근하도록 할 수 있다.

class SharedResource {
    private int counter = 0;

    // 동기화된 메서드
    synchronized void increment() {
        counter++;
    }

    synchronized int getCounter() {
        return counter;
    }
}

2) 동기화된 블록

특정 코드만 동기화하려면 메서드 대신 동기화된 블록을 사용할 수 있다.

class SharedResource {
    private int counter = 0;

    void increment() {
        synchronized (this) { // 동기화된 블록
            counter++;
        }
    }

    int getCounter() {
        return counter;
    }
}

 

4. 임계영역(Critical Section)과 경쟁 상태(Race Condition)

여러 스레드가 동시에 공유 자원에 접근할 때 경쟁 상태가 발생할 수 있다. 이를 방지하기 위해 임계영역을 보호해야 한다.

 

경쟁 상태 예시

class Counter {
    private int value = 0;

    // 경쟁 상태가 발생할 수 있음
    void increment() {
        value++;
    }

    int getValue() {
        return value;
    }
}

 

해결법: synchronized 사용

경쟁 상태를 방지하기 위해 increment() 메서드를 동기화한다.

class Counter {
    private int value = 0;

    synchronized void increment() {
        value++;
    }

    int getValue() {
        return value;
    }
}

 

 

5. 스레드 상태

  • NEW: 스레드가 생성되었지만, 아직 start() 메서드가 호출되지 않은 상태.
  • RUNNABLE: start() 메서드가 호출되어 실행 가능한 상태.
  • BLOCKED: 다른 스레드가 자원에 접근하고 있어 대기 중인 상태.
  • WAITING: wait() 메서드 등으로 다른 스레드가 자원을 반환할 때까지 대기하는 상태.
  • TIMED_WAITING: 지정된 시간 동안 대기하는 상태.
  • TERMINATED: 스레드의 실행이 종료된 상태.

6. 스레드 통신 (wait(), notify(), notifyAll())

스레드 간의 통신을 통해 서로 상태를 공유하거나, 대기와 알림을 관리할 수 있다.

1) wait() 메서드

wait() 메서드는 현재 스레드를 일시 정지시키고, 다른 스레드가 notify() 또는 notifyAll()을 호출할 때까지 대기한다.

class SharedResource {
    private int counter = 0;

    synchronized void increment() {
        while (counter == 5) {
            try {
                wait(); // 조건이 맞지 않으면 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        counter++;
        notify(); // 다른 스레드에게 알림
    }

    synchronized int getCounter() {
        return counter;
    }
}

2) notify() / notifyAll() 메서드

notify()는 대기 중인 스레드 중 하나를 깨우고, notifyAll()은 모든 대기 중인 스레드를 깨운다.

class SharedResource {
    private int counter = 0;

    synchronized void increment() {
        while (counter == 5) {
            try {
                wait(); // 대기
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        counter++;
        notify(); // 대기 중인 스레드 하나를 깨운다
    }

    synchronized void reset() {
        counter = 0;
        notifyAll(); // 모든 대기 중인 스레드를 깨운다
    }

    synchronized int getCounter() {
        return counter;
    }
}

 

7. 스레드 풀 (ExecutorService)

스레드 풀을 사용하면 스레드를 효율적으로 관리할 수 있다. ExecutorService는 일정 수의 스레드를 미리 생성하여 작업을 처리한다.

 

1) ExecutorService 사용 예

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        executor.submit(() -> {
            System.out.println("Task 1 executed");
        });

        executor.submit(() -> {
            System.out.println("Task 2 executed");
        });

        executor.submit(() -> {
            System.out.println("Task 3 executed");
        });

        executor.shutdown(); // 스레드 풀 종료
    }
}

ExecutorService를 사용하면 스레드를 효율적으로 관리하고, 불필요한 스레드 생성과 종료를 방지할 수 있다.

8. 멀티스레드 최적화 기법

  • Thread-local 변수 사용: 멀티스레드 환경에서 공유 자원을 사용해야 할 때, 각 스레드가 독립적인 값을 가지도록 ThreadLocal을 사용한다.
  • ForkJoinPool: ForkJoinPool은 큰 작업을 여러 작은 작업으로 나누어 병렬 처리하는 데 사용된다.
  • Future와 Callable 사용: Runnable은 반환값이 없지만, Callable은 작업의 결과를 반환할 수 있다. Future는 작업이 끝난 후 결과를 가져오는 데 사용된다.

9. 멀티스레드 관련 문제

  • 교착 상태(Deadlock): 여러 스레드가 서로의 자원을 기다리고 있어 실행이 멈추는 상태.
  • 활성화 부족(Livelock): 스레드가 계속해서 상태를 변경하지만 진행되지 않는 상태.
  • 스타베이션(Starvation): 스레드가 계속 실행되지 않고 대기하는 상태, 우선순위가 낮은 스레드가 자원을 얻지 못하는 상황.

'Language > Java' 카테고리의 다른 글

[JAVA] SOLID 원칙  (0) 2025.05.04
[JAVA] 상속  (0) 2025.05.04
[JAVA] 클래스  (0) 2025.05.04
[JAVA] 변수 위치 및 메모리 구조  (0) 2025.05.04
[JAVA] JVM  (0) 2025.05.04