본문 바로가기

개발/Java

[Java] 쓰레드 우선순위 / 쓰레드 그룹 / 쓰레드 동기화

쓰레드가 무엇인지, 어떤 종류가 있는지, 자바에서 어떻게 구현하는 지에 관한 내용을 아래의 링크에서 확인할 수 있다.

2024.04.27 - [개발/Java] - [Java] 프로세스와 쓰레드 ( Java에서 쓰레드 구현 / 싱글 쓰레드 / 멀티 쓰레드 / 데몬 쓰레드 / 사용자 쓰레드 )

 

이 글에서는 쓰레드 그룹과 동기화의 필요성과 우선순위 설정하는 방법에 대해 정리해보았다.

 

 

 

쓰레드 그룹

모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 한다. 자바에서는 따로 쓰레드 그룹을 지정하지 않으면 자동으로 main그룹에 포함된다.

 

 

쓰레드 그룹이 필요한 이유

 

코드 예시를 통해 쓰레드 그룹이 필요한 이유를 생각해보자.

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    break;
                }
            }
            System.out.println(Thread.currentThread().getName() + " Interrupted");
        };

        // ThreadGroup 클래스로 객체를 만듭니다.
        ThreadGroup group1 = new ThreadGroup("Group1");

        // Thread 객체 생성시 첫번째 매개변수로 넣어줍니다.
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread thread1 = new Thread(group1, task, "Thread 1");
        Thread thread2 = new Thread(group1, task, "Thread 2");

        // Thread에 ThreadGroup 이 할당된것을 확인할 수 있습니다.
        System.out.println("Group of thread1 : " + thread1.getThreadGroup().getName());
        System.out.println("Group of thread2 : " + thread2.getThreadGroup().getName());

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

        try {
            // 현재 쓰레드를 지정된 시간동안 멈추게 합니다.
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // interrupt()는 일시정지 상태인 쓰레드를 실행대기 상태로 만듭니다.
        group1.interrupt(); //group으로 묶어서 제어
      

    }
}

 

위에선 2개의 쓰레드만 사용했지만, 쓰레드들이 10개, 100개 실행되고 있는 상황이라면 하나하나 쓰레드들을 개별적으로 처리해주어야하는 번거로움이 있는데 이처럼 쓰레드 그룹이 있으면 한꺼번에 쓰레드를 제어할 수 있다는 장점이 있다.

 

 

 

 

 

 

 

쓰레드 우선순위

 쓰레드 작업의 중요도에 따라서 쓰레드의 우선순위를 부여할 수 있다. 작업의 중요도가 높을 때 우선순위를 높게 지정하면 더 많은 작업시간을 부여받아 빠르게 처리될 수 있다. 우선순위가 높다는 것은 많은 작업시간을 할당받을 확률이 높아지는 것이지 반드시 쓰레드가 먼저 종료되는 것은 아니다.

 

 

우선순위는 아래와 같이 3가지 (최대/최소/보통) 우선순위로 나뉜다.

 

1. 최대 우선순위 (MAX_PRIORITY) = 10

2. 최소 우선순위 (MIN_PRIORITY) = 1

3. 보통 우선순위 (NROM_PRIORITY) = 5

 

기본 값은 보통 우선순위이고, 더 자세하게 나눈다면 1~10 사이의 숫자로 지정 가능하다.

 

 

우선순위 예시 코드

Thread thread1 = new Thread(task1);
thread1.setPriority(8); //우선순위 설정

int threadPriority = thread1.getPriority(); //우선순위 반환하여 확인
System.out.println("threadPriority = " + threadPriority);

 

 

 

 

 

 

 

쓰레드 동기화 (Synchronized)

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 침범하지 못하도록 막는 것을 '쓰레드 동기화(Synchronization)'라고 한다. 동기화를 하려면 다른 쓰레드의 침범을 막아야 하는 코드들을 ‘임계 영역’으로 설정하면 된다. 임계 영역은 한 번에 한 쓰레드만 사용이 가능다.

 

 

 

임계 영역 지정 방법

 

1. 메소드 임계영역 지정

public synchronized void asyncSum() {
	  //침범을 막아야하는 코드
}

 

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

synchronized(해당 객체의 참조변수) {
		//침범을 막아야하는 코드
}

 

이렇게 앞에 synchronized를 붙여주면 된다.

 

 

 

쓰레드 동기화가 필요한 이유

 

쓰레기 동기화가 필요한 예시를 코드를 통해 확인해보자.

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

        Runnable task = () -> {
            while (appleStore.getStoredApple() > 0) { //사과가 남았으면
                appleStore.eatApple(); //사과를 제거하고
                System.out.println("남은 사과의 수 = " + appleStore.getStoredApple()); //남은 사과 수 출력하는 작업
            }

        };

        for (int i = 0; i < 3; i++) { //쓰레드 3개 생성하여 실행.
            new Thread(task).start();
        }
    }
}

class AppleStore {
    private int storedApple = 10;

    public int getStoredApple() {
        return storedApple;
    }

    public void eatApple() {
        synchronized (this) { //동기화 설정 (쓰레드 하나가 작업 중이면 나머지 쓰레드들은 기다려야함.
            if(storedApple > 0) { //만약 동기화 설정 해주지 않으면 사과가 1개 남았을 때 여기서 문제 발생 
            //( 3개의 쓰레드 모두 처음에 확인 했을 땐, 0개 이상이었을 것이기에 쓰레드 3개 모두 먹으려 시도할 것.
            // 남은 1개를 3개 쓰레드가 모두 먹어서 남은 사과 수가 -값이 나오게 됨.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                storedApple -= 1;
            }
        }
    }
}

 

위의 예시를 확인해보면, 만약 eatApple 함수에서 동기화 처리를 해주지 않는다면, 사과박스에 사과가 1개 남았을 때 쓰레드 3개가 모두 1개 남았다고 생각하고 다 먹어버려서 남은 사과의 값이 -값이 나올 가능성이 있다.

 

그래서 if(storedApple > 0)에 동기화 작업을 해주어 하나의 쓰레드가 확인 중이면 나머지 쓰레드는 기다리게 처리하여 남은 사과 값이 정상적으로 0개로 나올 수 있게 해줄 수 있다.