Java 스레드(Thread) 개념 및 활용

스레드는 Java 프로그래밍에서 중요한 개념 중 하나입니다. 먼저 프로세스와 스레드의 차이점을 간단히 살펴보겠습니다.

프로세스(Process)란 운영체제에서 실행 중인 하나의 프로그램을 의미하며, 시스템 자원을 할당받는 기본 단위입니다. 각각의 프로세스는 독립된 메모리 공간과 코드를 가지고 있습니다.

스레드(Thread)는 프로세스 내부에서 실행되는 경량화된 작업 흐름으로, 동일한 프로세스 내에서는 메모리를 공유하지만 각각 별도의 스택 영역과 프로그램 카운터(PC)를 유지합니다. 이 때문에 스레드 간 전환 비용이 상대적으로 적습니다.

Java에서 스레드 생성 방법

Java에서는 java.lang.Thread 클래스를 통해 스레드를 구현할 수 있습니다. JVM이 시작될 때 main() 메서드가 있는 주 스레드(main thread)가 생성됩니다. 새로운 스레드는 두 가지 방식으로 만들 수 있습니다:

  • Runnable 인터페이스 구현
  • Thread 클래스 상속
public class ThreadExample {
    public static void main(String[] args) {
        // 첫 번째 방법: Runnable 인터페이스 사용
        Task task = new Task();
        Thread worker1 = new Thread(task);

        // 두 번째 방법: Thread 클래스 확장
        Worker worker2 = new Worker();

        // 스레드 시작
        worker1.start();
        worker2.start();

        // 메인 스레드 작업
        for (int i = 0; i < 50; i++) {
            System.out.println("메인 스레드 실행");
        }
    }
}

class Task implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("작업 스레드 1");
        }
    }
}

class Worker extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("작업 스레드 2");
        }
    }
}

스레드 제어 관련 메서드

스레드 실행 흐름을 조절하기 위해 다음과 같은 메서드들이 제공됩니다:

  • sleep(long millis): 현재 스레드를 지정된 시간(ms)만큼 일시 정지
  • join(): 다른 스레드의 작업이 완료될 때까지 대기
  • yield(): 현재 스레드가 CPU를 양보하고 다른 스레드에게 실행 기회 부여
public class ControlExample {
    public static void main(String[] args) {
        BackgroundTask background = new BackgroundTask();
        ForegroundWorker foreground = new ForegroundWorker();

        Thread bgThread = new Thread(background);
        Thread fgThread = new Thread(foreground);

        bgThread.start();
        fgThread.start();

        try {
            bgThread.join(); // 백그라운드 작업 종료까지 대기
            Thread.sleep(3000); // 3초간 일시정지
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 50; i++) {
            if (i == 25) {
                Thread.yield(); // CPU 양보
            }
            System.out.println("메인 처리");
        }
    }
}

class BackgroundTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("백그라운드 작업");
        }
    }
}

class ForegroundWorker extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println("포그라운드 작업");
        }
    }
}

스레드 우선순위 설정

JVM은 스레드 스케줄러를 통해 준비 상태에 있는 모든 스레드를 감시하며, 우선순위(priority)에 따라 어떤 스레드를 실행할지 결정합니다. 우선순위는 1(MIN_PRIORITY)부터 10(MAX_PRIORITY)까지의 정수값으로 표현되며, 기본값은 5(NORM_PRIORITY)입니다.

// 우선순위 조회 및 설정
int currentPriority = thread.getPriority();
thread.setPriority(Thread.MAX_PRIORITY);

스레드 동기화(Synchronization)

여러 스레드가 동시에 공유 자원에 접근할 때 데이터 불일치가 발생할 수 있습니다. 이를 해결하기 위해 Java는 synchronized 키워드를 제공하여 특정 코드 블록이나 메서드에 잠금(Lock)을 적용할 수 있습니다.

public class SynchronizationDemo implements Runnable {
    SharedResource resource = new SharedResource();

    public static void main(String[] args) {
        SynchronizationDemo demo = new SynchronizationDemo();
        
        Thread thread1 = new Thread(demo, "Thread-1");
        Thread thread2 = new Thread(demo, "Thread-2");
        
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        resource.increment(Thread.currentThread().getName());
    }
}

class SharedResource {
    private static int counter = 0;

    public synchronized void increment(String threadName) {
        counter++;
        try {
            Thread.sleep(1);
            System.out.println(threadName + "가 " + counter + "번째 접근");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

데드락(Deadlock) 예시

두 개 이상의 스레드가 서로가 보유한 자원을 기다리느라 무한 대기 상태에 빠지는 현상입니다. 아래는 데드락이 발생할 수 있는 상황을 보여주는 예제입니다:

public class DeadlockDemo implements Runnable {
    private int mode = 1;
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        DeadlockDemo demo1 = new DeadlockDemo();
        DeadlockDemo demo2 = new DeadlockDemo();
        
        demo1.mode = 1;
        demo2.mode = 0;
        
        Thread thread1 = new Thread(demo1);
        Thread thread2 = new Thread(demo2);
        
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        System.out.println("mode=" + mode);

        if (mode == 1) {
            synchronized (lockA) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("Thread 1 completed");
                }
            }
        }

        if (mode == 0) {
            synchronized (lockB) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println("Thread 2 completed");
                }
            }
        }
    }
}

이처럼 Java의 스레드는 병렬 프로그래밍을 가능하게 하는 핵심 요소이며, 올바른 동기화 기법을 통해 안전한 멀티스레딩 프로그램을 작성할 수 있습니다.

태그: java Thread synchronization deadlock Multithreading

7월 1일 23:57에 게시됨