Languages/Java

[Java] 스레드 (Thread)

dbssk 2023. 8. 31. 15:30

요즘 OS는 모두 멀티태스킹을 지원하는데 멀티태스킹을 가능하게 하는 요소 중 하나가 바로 '멀티스레딩'이다. 멀티스레딩은 하나의 프로세스 안에서 여러 개의 스레드가 동시에 작업을 수행하는 것을 의미한ㄷ.

스레드 구현

자바에서 스레드를 구현하는 방법은 크게 두 가지이다. 하나는 'Runnable' 인터페이스를 구현하는 방법이고, 다른 하나는 'Thread' 클래스를 상속하는 방법이다.

1. Runnable 인터페이스 구현

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 스레드가 수행할 작업 내용
    }
}

2. Thread 클래스 상속

public class MyThread extends Thread {
    @Override
    public void run() {
        // 스레드가 수행할 작업 내용
    }
}

 

스레드 생성과 실행

스레드를 생성하기 위해서는 위에서 언급한 방법으로 구현한 클래스를 인스턴스화하여 'start()' 메서드를 호출해야 한다. 'start()' 메서드를 호출하면 JVM은 해당 스레드에게 별도의 콜 스택을 할당하고 작업을 수행하게 된다. 작업을 수행하기 위해 'run()' 메서드가 아닌 'start()' 메서드를 호출해야 한다는 것에 유의해야한다.

public class ThreadExample {

    public static void main(String[] args) {
        // Runnable 인터페이스를 구현한 클래스의 인스턴스 생성
        MyRunnable myRunnable = new MyRunnable();
        
        // Thread 클래스의 생성자에 Runnable 인스턴스를 전달하여 스레드 생성
        Thread thread1 = new Thread(myRunnable);
        
        // Thread 클래스를 상속한 클래스의 인스턴스 생성
        MyThread myThread = new MyThread();
        
        // start() 메서드를 호출하여 스레드 실행
        thread1.start();
        myThread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnable 스레드: " + i);
            try {
                Thread.sleep(500); // 0.5초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread 상속 스레드: " + i);
            try {
                Thread.sleep(500); // 0.5초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

스레드의 상태와 실행 제어

스레드는 여러 상태를 가질 수 있으며, NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TERMINATED 등이 있다. 스레드의 상태는 해당 스레드의 생명 주기와 관련이 있다.

  • NEW : 스레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능 상태
  • BLOCKED : 동기화 블럭에 의해 일시정지된 상태
  • WAITING, TIME_WAITING : 실행가능하지 않은 일시정지 상태
  • TERMINATED : 스레드 작업이 종료된 상태

스레드 간 협력 작업을 위해 'wait()'와 'notify()' 메서드를 활용할 수 있다. 'wait()' 메서드는 스레드를 Not Runnable 상태로 전환하며, 'notify()' 메서드는 대기 중인 스레드에게 다시 Runnable 상태로 전환할 수 있는 기회를 준다.

 

스레드 동기화

멀티스레딩 환경에서는 여러 스레드가 공유 자원에 접근할 때 동기화 문제가 발생할 수 있다. 이를 해결하기 위해 'synchronized' 키워드를 사용하여 임계영역과 lock을 활용하고 'wait()'와 'notify()'를 활용할 수도 있다.

 

동기화 방법

  • 임계 영역(Critical Section) : 공유 자원에 대한 접근을 하나의 스레드만 가능하도록 제한하여 동시에 여러 스레드가 해당 자원에 접근하지 못하도록 보호하는 메커니즘
  • 뮤텍스(Mutex) : 공유 자원에 대한 접근을 한 번에 하나의 스레드만 허용하도록 제어하는 동기화 방법이다. 뮤텍스는 보통 하나의 프로세스 내에서 사용되며, 여러 스레드가 경쟁적으로 자원에 접근하는 것을 방지한다.
  • 이벤트(Event) : 특정한 사건이 발생하거나 조건이 충족되면 다른 스레드에게 알림을 보내는 동기화 방법
  • 세마포어(Semaphore) : 한정된 개수의 자원을 여러 스레드가 동시에 사용하려 할 때, 접근 가능한 스레드의 개수를 제어하는 동기화 기법이다. 세마포어는 카운터 변수를 통해 스데르의 허용 가능 개수를 관리하며, 스레드가 자원을 사용하면 카운터가 감소하고 반납하면 증가한다.
  • 대기 가능 타이머(Waitable Timer) : 특정 시간이 지나면 대기 중인 스레드를 깨워주는 동기화 방법이다. 스레드가 일시적으로 대기 상태로 들어가면서 특정 시간 후에 다시 실행될 수 있게 해준다.

 

synchronized 키워드 사용

public class SynchronizationExample {

    private int sharedCounter = 0; // 여러 스레드가 공유하는 변수

    public synchronized void incrementCounter() {
        sharedCounter++; // 공유 변수를 증가시키는 메서드
        System.out.println("Counter value: " + sharedCounter);
    }

    public static void main(String[] args) {
        SynchronizationExample example = new SynchronizationExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.incrementCounter(); // 스레드 1이 공유 변수를 증가시킴
                try {
                    Thread.sleep(500); // 0.5초 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                example.incrementCounter(); // 스레드 2가 공유 변수를 증가시킴
                try {
                    Thread.sleep(500); // 0.5초 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread1.start(); // 스레드 1 시작
        thread2.start(); // 스레드 2 시작
    }
}

wait()과 notify() 활용

스레드 간의 협력 작업을 강화하기 위해 try-catch문 내에 적절히 사용하면 좋다.