Java 스레드: 생성, 관리 및 동기화

Java에서 스레드는 독립적인 실행 흐름을 제공하여 애플리케이션의 동시성을 높입니다. 스레드를 생성하고 관리하는 다양한 방법을 이해하는 것은 효과적인 멀티스레드 애플리케이션을 구축하는 데 필수적입니다.

스레드 생성 방법

Java에서 스레드를 생성하는 주요 방법은 다음과 같습니다.

1. Thread 클래스 상속

Thread 클래스를 상속받아 사용자 정의 스레드 클래스를 만들고 run() 메서드를 재정의합니다. run() 메서드는 스레드가 시작될 때 실행될 코드를 포함합니다.


class CustomThread extends Thread {
    @Override
    public void run() {
        System.out.println("스레드 실행 중!");
    }
}

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

2. Runnable 인터페이스 구현

Runnable 인터페이스를 구현하여 스레드의 작업을 정의하고, Thread 객체를 생성할 때 해당 Runnable 객체를 생성자의 인자로 전달합니다. 이 방법은 클래스 다중 상속의 제약을 피할 수 있어 더 유연합니다.


class CustomRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable 스레드 실행 중!");
    }
}

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

3. 익명 내부 클래스를 이용한 스레드 생성

Thread 클래스를 상속하거나 Runnable 인터페이스를 구현하는 익명 내부 클래스를 사용하여 스레드를 더 간결하게 생성할 수 있습니다.


public class AnonymousThreadDemo {
    public static void main(String[] args) {
        // Thread 상속 익명 내부 클래스
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                System.out.println("익명 Thread 클래스 실행 중!");
            }
        };
        thread1.start();

        // Runnable 구현 익명 내부 클래스
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("익명 Runnable 클래스 실행 중!");
            }
        });
        thread2.start();
    }
}
    

4. 람다 표현식을 이용한 스레드 생성

Runnable 인터페이스는 함수형 인터페이스이므로, 람다 표현식을 사용하여 더욱 간결하게 스레드를 생성할 수 있습니다. 이는 코드를 훨씬 짧고 읽기 쉽게 만듭니다.


public class LambdaThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("람다 스레드 실행 중!");
        });
        thread.start();
    }
}
    

start()run() 메서드의 차이

start() 메서드는 새로운 스레드를 생성하여 해당 스레드의 run() 메서드를 실행하도록 예약합니다. 반면, run() 메서드를 직접 호출하는 것은 새로운 스레드를 생성하지 않고 현재 스레드에서 run() 메서드의 코드를 실행하는 것과 같습니다.


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

public class StartVsRunDemo {
    public static void main(String[] args) {
        MyThread thread = new MyThread();

        System.out.println("--- start() 호출 ---");
        thread.start(); // 새 스레드 생성 및 run() 실행

        System.out.println("--- run() 직접 호출 ---");
        thread.run();   // 현재 스레드(main)에서 run() 실행
    }
}
    

start()를 사용하면 여러 스레드가 병렬적으로 실행될 수 있지만, run()을 직접 호출하면 단일 스레드에서 순차적으로 실행됩니다.

스레드 속성 및 메서드

1. 스레드 ID 및 이름

각 스레드는 JVM에 의해 고유한 ID와 이름을 가집니다. 디버깅 시 유용합니다.

  • getId(): 스레드의 고유 ID를 반환합니다.
  • getName(): 스레드의 이름을 반환합니다.
  • Thread(Runnable target, String name) 생성자: 스레드 이름 지정 가능

public class ThreadInfoDemo {
    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            System.out.println("작업 스레드 ID: " + Thread.currentThread().getId());
            System.out.println("작업 스레드 이름: " + Thread.currentThread().getName());
        }, "WorkerThread"); // 스레드 이름 지정

        worker.start();

        System.out.println("메인 스레드 ID: " + Thread.currentThread().getId());
        System.out.println("메인 스레드 이름: " + Thread.currentThread().getName());
    }
}
    

2. 스레드 상태

스레드는 NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED와 같은 다양한 상태를 가집니다. getState() 메서드로 현재 상태를 확인할 수 있습니다.

3. 우선순위

스레드는 우선순위를 가질 수 있으며, 우선순위가 높은 스레드가 CPU 시간을 더 많이 할당받을 가능성이 있습니다. 하지만 이는 운영체제 스케줄러에 따라 달라질 수 있어 절대적인 보장은 없습니다. 기본 우선순위는 5입니다.

  • getPriority(): 스레드의 우선순위를 반환합니다.
  • setPriority(int newPriority): 스레드의 우선순위를 설정합니다. (1-10)

4. 데몬 스레드 (Daemon Thread)

데몬 스레드는 일반 스레드(전경 스레드)가 모두 종료되면 함께 종료되는 보조적인 스레드입니다. JVM은 실행 중인 전경 스레드가 하나라도 있으면 종료되지 않습니다. 데몬 스레드로 설정하면 프로그램의 주 스레드가 모두 종료될 때 자동으로 종료됩니다. setDaemon(true)를 사용하여 설정하며, start() 호출 전에 설정해야 합니다.


public class DaemonThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread daemon = new Thread(() -> {
            while (true) {
                System.out.println("데몬 스레드 작동 중...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }, "DaemonWorker");

        daemon.setDaemon(true); // 데몬 스레드로 설정
        daemon.start();

        System.out.println("메인 스레드 종료");
        // 메인 스레드가 종료되면 데몬 스레드도 함께 종료됩니다.
    }
}
    

5. 스레드 생존 확인

isAlive() 메서드는 스레드가 현재 실행 중인지 (NEW 또는 TERMINATED 상태가 아닌지) 확인합니다.

스레드 제어 메서드

1. sleep(): 현재 스레드 일시 중지

Thread.sleep(long millis)` 메서드는 현재 실행 중인 스레드를 지정된 시간(밀리초) 동안 일시 중지시킵니다. 이 메서드는 InterruptedException을 발생시키므로 예외 처리가 필요합니다.


public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("시작");
        Thread.sleep(2000); // 메인 스레드 2초간 일시 중지
        System.out.println("2초 후 종료");
    }
}
    

2. join(): 다른 스레드의 완료 대기

thread.join()` 메서드는 호출한 스레드가 지정된 스레드(thread)가 종료될 때까지 대기하도록 합니다. 이는 스레드 간의 실행 순서를 제어하는 데 사용됩니다. join(long millis)` 형태로 시간 제한을 둘 수도 있습니다.


public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("작업 스레드 시작");
            try {
                Thread.sleep(3000); // 3초 작업
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("작업 스레드 완료");
        });

        worker.start();
        worker.join(); // 작업 스레드가 완료될 때까지 메인 스레드 대기

        System.out.println("메인 스레드 계속 실행");
    }
}
    

3. 스레드 종료: interrupt() 와 플래그

스레드를 안전하게 종료하는 방법에는 여러 가지가 있습니다. 직접적인 종료 메서드는 없으므로, 스레드 내부에서 종료 조건(플래그 또는 isInterrupted() 상태)을 확인하고 스스로 종료하도록 구현해야 합니다.

  • 플래그 사용: 멤버 변수로 boolean 플래그를 두고, 이 플래그를 기준으로 run() 메서드 내에서 루프를 종료합니다.
  • interrupt() 메서드: 다른 스레드에서 해당 스레드의 interrupt() 메서드를 호출하여 인터럽트 상태 플래그를 설정합니다. 스레드 내부에서는 Thread.currentThread().isInterrupted()로 이 플래그를 확인하고, sleep() 또는 join()과 같은 블로킹 메서드 중에 인터럽트가 발생하면 InterruptedException이 발생합니다.

public class InterruptDemo {
    // 플래그 사용 예시
    private static volatile boolean quitFlag = false;

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (!quitFlag) {
                System.out.println("작업 중...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    // sleep 중 인터럽트 발생 시, isInterrupted() 상태가 true가 됨
                    System.out.println("인터럽트 발생!");
                    Thread.currentThread().interrupt(); // 인터럽트 상태 재설정 (필요 시)
                    break; // 루프 종료
                }
            }
            System.out.println("작업 스레드 종료");
        });

        worker.start();

        Thread.sleep(2000); // 2초 후 종료

        // 방법 1: 플래그 변경
        // quitFlag = true;
        // System.out.println("플래그 변경하여 종료 요청");

        // 방법 2: interrupt() 호출
        worker.interrupt();
        System.out.println("interrupt() 호출");
    }
}
    

volatile 키워드는 여러 스레드 간에 플래그 값이 일관되게 유지되도록 보장합니다. sleep()이나 join() 중에 인터럽트가 발생하면 InterruptedException이 발생하며, 이 예외를 catch 블록에서 처리하여 스레드를 종료시킬 수 있습니다.

태그: java 스레드 멀티스레딩 Thread Runnable

7월 27일 01:01에 게시됨