쓰레드 제어 메서드 2 (join, yield, synchoronized)

KDG: First things first!·2024년 7월 31일
0

CS

목록 보기
3/6



이 포스팅은 이전 포스팅인 쓰레드 상태와 제어 메서드1 (sleep, interrupt)에 이어집니다.



쓰레드 제어 메서드

3. join()

join() : 정해진 시간 동안 지정한 쓰레드가 작업하는 것을 기다리도록 하는 메서드이다.

join() 메서드를 실행할 때 ms(시간)을 따로 설정하지 않으면 지정한 쓰레드의 작업이 끝날 때까지 기다린다.

Thread thread = new Thread(task, "thread");

thread.start();

try {
	thread.join(); // 해당 쓰레드의 작업이 종료될 때까지 메인 쓰레드가 기다린다.
} catch (InterruptedException e) {
	e.printStackTrace();
}

join()도 앞서 다루었던 sleep()처럼 ms(밀리초) 단위로 시간을 설정할 수 있고 interrupt()을 만나면 기다리던 것을 멈추기 때문에 InterruptedException이 발생할 수 있다. 이를 처리하기 위해 마찬가지로 try-catch로 예외처리를 해야 한다.

해당 예제 코드에서는 따로 시간을 지정하지 않았기 때문에 메인 쓰레드는 해당 쓰레드가 작업을 완전히 끝마쳐 종료될 때까지 종료되지 않고 기다린다.


public class Main {
    public static void main(String[] args) {
    
        Runnable task = () -> {
            try {
                Thread.sleep(5000); // 5초
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(task, "thread"); // NEW 상태

        thread.start(); // NEW -> RUNNABLE 상태

        long start = System.currentTimeMillis();

        try {
            thread.join(); // 시간 설정 안했기 때문에 메인 쓰레드는 이 쓰레드가 종료될 때까지 기다려준다.

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // thread 의 소요시간인 5000ms 동안 main 쓰레드가 기다렸기 때문에 5000이상이 출력됩니다.
        System.out.println("소요시간 = " + (System.currentTimeMillis() - start));
    }
}

// 출력
/* 소요시간: 5005 */



4. yield()

yield : 쓰레드가 자신에게 주어진 남은 시간과 자원을 자신보다 우선 순위가 높거나 같은 쓰레드에게 양보하고 해당 쓰레드는 실행 대기 상태로 만드는 메서드이다.


public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                }
            } catch (InterruptedException e) {
                Thread.yield();
            }
        };

        Thread thread1 = new Thread(task, "thread1");
        Thread thread2 = new Thread(task, "thread2");

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread1.interrupt();

    }
}

// 출력 결과
/*
thread2
thread1
thread2
thread1
thread2
thread1
thread2
thread1
thread2
thread2
thread2
thread2
thread2
thread2
*/

쓰레드 1, 2가 생성되고 task 안의 로직이 실행된다.

task 안의 로직은 1초씩 10번을 반복해야 되기 때문에 실행되는데 10초 이상의 시간이 걸린다.

이후 task 안의 작업이 반복되는 10초 동안 sleep()이 실행되어 일시 정지 상태가 되어 버린다.

그 직후 interrupt()가 실행되어 쓰레드 1은 실행 대기 상태가 되어 버려 InterruptedException이 발생하여 catch문 안의 yield()가 실행되어 쓰레드 1는 남은 자원들을 쓰레드 2한테 양보하고 자신은 실행 대기 상태가 된다.

출력 결과를 보면 interrupt() 전까지는 쓰레드 1, 2가 거의 돌아가면서 동시에 출력되다가
쓰레드 1에 interrupt()가 실행되면서 쓰레드 2만 출력되는 것을 볼 수 있다.




5. synchoronized

synchoronized() : 멀티 쓰레드에서는 복수의 쓰레드가 한 프로세스의 자원을 공유하여 사용하기 때문에 서로에게 영향을 줄 수 있고 이로 인하여 에러와 버그가 발생할 수 있다.

이러한 일을 방지하기 위해 한 쓰레드가 실행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 '쓰레드 동기화(synchoronization)'이라고 한다.

동기화를 위해서는 쓰레드가 작업 중인 코드들을 Lock를 가진 오직 단 하나의 쓰레드만이 출입이 가능하게 설정하여 다른 쓰레드들이 침법하지 못하는 '임계 영역'으로 설정해야 한다.



실행할 메서드 또는 실행할 코드 묶음 앞에 synchoronized를 붙이면 임계영역이 지정되어 Lock이 걸려 다른 쓰레드들이 침입을 못하게 막을 수 있다.

1. 메서드 전체를 임계영역으로 지정

public synchronized void synchronizedTestClass() {
       // 침입 방지 코드
}

2. 특정 영역만 임계영역으로 지정

synchronized(해당 객체 참조 변수) {
      // 침입 방지 코드
}

[동기화 문제 발생 예시 코드]

public class Main {
    public static void main(String[] args) {
        AppleStore appleStore = new AppleStore();

        Runnable task = () -> {
            while (appleStore.getStoredApple() > 0) { // 쓰레드 1, 2, 3이 각각 독립적으로 남은 사과 수 체크하여 동시에 진입
                appleStore.eatApple(); // 각 쓰레드가 1초씩 기다렸다가 사과를 먹는 행위를 조건 충족할 때까지 반복

                /* 결국 사과가 1개 남았을 때 조건 통과해서 진입은 세 쓰레드가 동시에 모두 성공했는데 그 중 한 쓰레드가 먹어버리면 
                사과 개수는 0개인데 아직 두 쓰레드가 먹어야할 2개가 있어야 한다는 문제 발생*/
                System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());   
            }

        };

        for (int i = 0; i < 3; i++) {
            new Thread(task).start(); // 쓰레드 차례로 3개 생성 및 각자 start
        }
    }
}

class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
        if (storedApple > 0) {
            try {
                Thread.sleep(1000); //
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            storedApple -= 1;
        }
    }
}

// 출력 결과
/*
남은 사과의 수 = 9
남은 사과의 수 = 7
남은 사과의 수 = 8
남은 사과의 수 = 6
남은 사과의 수 = 5
남은 사과의 수 = 4
남은 사과의 수 = 3
남은 사과의 수 = 1
남은 사과의 수 = 1
남은 사과의 수 = 0
남은 사과의 수 = -1
남은 사과의 수 = -2
*/

이미 한 쓰레드가 작업 중인 영역을 다른 쓰레드들이 마음껏 침범하여 사과가 0개임에도 사과 2개를 더 먹어버리는 동기화 문제가 발생했다.


public class Main {
    public static void main(String[] args) {
        AppleStore appleStore = new AppleStore();

        Runnable task = () -> {
            while (appleStore.getStoredApple() > 0) { // 가장 먼저 한 쓰레드가 도달하면 임계 영역으로 Lock 걸려서 다른 쓰레드 접근 불가
                appleStore.eatApple(); // 해당 임계 영역 점유한 쓰레드가 처음부터 끝까지 홀로 작업 수행
                
                System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
            }

        };

        for (int i = 0; i < 3; i++) {
            new Thread(task).start(); // 쓰레드 차례로 3개 생성 및 각자 start
        }
    }
}

class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
        synchronized (this) { // synchronized()로 한 쓰레드만 통과할 수 있는 임계 영역으로 설정 

            if (storedApple > 0) {
                try {
                    Thread.sleep(1000); //
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                storedApple -= 1;
            }
        }
    }
}

synchronized(this)로 eatApple() 내부 영역을 묶어버리면 한 쓰레드가 해당 영역을 점유해서 작업 중이면 다른 쓰레드들은 해당 영역에 접근할 수 없다.

여기서는 세 쓰레드 중 한 쓰레드가 eatApple()에 가장 먼저 도달하면 다른 두 쓰레드는 이 메서드 로직이 끝날 때까지 접근할 수 없고 기다려야 한다.

이후 첫 번째 쓰레드가 작업을 모두 끝내면 사과의 개수는 어차피 0이 되어서 나머지 두 쓰레드는 조건문을 통과하지 못하여 로직 수행이 불가하다.

profile
알고리즘, 자료구조 블로그: https://gyun97.github.io/

0개의 댓글