개발 낙서장

[TIL] 내일배움캠프 10일차 - 쓰레드 - 본문

Java/Sparta

[TIL] 내일배움캠프 10일차 - 쓰레드 -

권승준 2024. 1. 8. 19:44

 

 

 

 

오늘의 학습 키워드📚

쓰레드

  • 프로세스 내에서 일하는 일꾼
  • 싱글 쓰레드
    • 프로세스 안에서 하나의 쓰레드만 실행되는 것을 말한다.
    • 대표적으로 자바에서 main() 메서드만 실행했을 경우 이것을 싱글 쓰레드라 한다.
    • 하나의 쓰레드가 끝나야 다음 쓰레드로 넘어가기에 절차적이지만 어느 쓰레드에서 지연이 발생하면 프로세스 전체가 지연이 되는 단점이 있다.
  • 멀티 쓰레드
    • 프로세스 안에서 여러개의 쓰레드가 실행되는 것을 말한다.
    • 여러 쓰레드를 동시에 작업할 수 있어 성능이 좋아지고 스택을 제외한 모든 영역에서 메모리를 공유하기에 메모리 효율이 증가한다.
    • 여러 쓰레드들이 자원을 공유하기에 동기화 문제가 발생할 수 있고 둘 이상의 쓰레드가 서로의 자원을 요청할 때 서로 작업이 종료되기만을 기다리는 교착 상태(데드락)가 발생할 수 있다.

쓰레드 구현

  • Thread
public class TestThread extends Thread {
				@Override
				public void run() {
							// 쓰레드 수행작업
				}
}

...

TestThread thread = new TestThread(); // 쓰레드 생성
thread.start() // 쓰레드 실행
  • Runnable
public class TestRunnable implements Runnable {
				@Override
				public void run() {
							// 쓰레드 수행작업 
				}
}

...

Runnable run = new TestRunnable();
Thread thread = new Thread(run); // 쓰레드 생성

thread.start(); // 쓰레드 실행

잘 보면 둘의 차이점이 보인다. Thread는 Class이기 때문에 extends 해야 하지만 Runnable은 Interface로 implements 하고 있다.
자바는 다중 상속이 불가능한데 Thread를 상속받을 경우 다른 클래스로의 확장이 불가능해지기 때문에 Runnable을 구현하는 것이 확장성에 유리하다.

Runnable은 람다식으로 구현할 수 있다.

public class Main {
    public static void main(String[] args) {
        Runnable task = () -> {
            // 구현
        };

        Thread thread1 = new Thread(task);
        thread1.setName("thread1");

        thread1.start();
    }
}
  • 멀티 쓰레드
public class Main {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            System.out.println("task1");
            for(int i = 0; i < 10; i++) {
                System.out.printf("%d, ", i);
            }
            System.out.println("\n");
        };
        Runnable task2 = () -> {
            System.out.println("task2");
            for(int i = 0; i < 10; i++) {
                System.out.printf("%d- ", i);
            }
            System.out.println("\n");
        };


        Thread thread1 = new Thread(task1);
        thread1.setName("thread1");
        Thread thread2 = new Thread(task2);
        thread2.setName("thread2");

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

이런 쓰레드를 구현했다고 해보자. task1은 '숫자, '를 반복해서 출력하고 task2는 '숫자- '를 반복해서 출력한다.
우리가 알고 있는 일반적인 프로세스라면 task1이 먼저 실행되고 task2가 먼저 실행될 것이다.

이렇게 결과가 뒤죽박죽이 된다. 여기서 알 수 있는 점은 별도의 작업을 하지 않는 한 Runnable은 순서를 보장하지 않는다.
두 쓰레드가 번갈아 실행되기 때문이다.

  • 사용자 쓰레드, 데몬 쓰레드

사용자 쓰레드란 보이는 곳에서 실행되는 우선 순위가 높은 쓰레드를 말한다. 프로세스의 기능 부분을 담당하며 메인 쓰레드 등 앞에서 구현했던 모든 쓰레드가 사용자 쓰레드에 속한다.

반대로 데몬 쓰레드는 백그라운드에서 실행되는 우선 순위가 낮은 쓰레드를 말한다. 보조적인 역할을 담당하며 대표적인 데몬 쓰레드로는 가비지 컬렉터(GC)가 있다.

public class Main {
    public static void main(String[] args) {
        Runnable demon = () -> {
            for (int i = 0; i < 1000000; i++) {
                System.out.printf("demon" + i + ", ");
            }
        };

        Thread thread = new Thread(demon);
        thread.setDaemon(true); // true로 설정시 데몬스레드로 실행됨

        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.printf("task" + i + ", ");
        }
    }
}

데몬 쓰레드는 우선 순위가 낮고 다른 쓰레드가 모두 종료되면 데몬 쓰레드 또한 작업이 끝나지 않았다 하더라도 강제 종료된다.

 

task가 끝나 task99까지 실행되고 종료되므로 데몬 쓰레드 또한 얼마 가지 않아 바로 종료돼버린다.

쓰레드 우선순위

사용자 쓰레드와 데몬 쓰레드 말고도 쓰레드 자체적으로 중요도에 따라 우선 순위를 지정할 수 있다.
우선 순위가 높아질 수록 더 많은 작업 시간을 부여받아 빠르게 처리될 수 있다.
하지만 우선 순위가 높다고 해서 무조건 먼저 끝나는 것은 아니다. 우선적으로 처리되기 때문에 먼저 끝날 확률이 높은 것일 뿐이다.

우선 순위는 최소 1부터 최대 10까지 존재하며 그 사이의 숫자 중에서 더 자세하게 나눌 수 있다.
우선 순위는 setPriority(int) 메소드로 부여할 수 있으며 getPriority() 메소드로 확인할 수 있다.

public class Main {
    public static void main(String[] args) {
        Runnable task1 = () -> {
            for (int i = 0; i < 50; i++) {
                System.out.print("$");
            }
        };

        Runnable task2 = () -> {
            for (int i = 0; i < 40; i++) {
                System.out.print("*");
            }
        };

        Thread thread1 = new Thread(task1);
        thread1.setPriority(8);

        Thread thread2 = new Thread(task2);
        thread2.setPriority(2);

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

 

task1이 50번, task2가 40번의 작업량으로 task2가 원래는 먼저 끝나야 하지만 우선 순위가 쓰레드1이 더 높기 때문에 쓰레드1의 작업이 먼저 끝난 후에 쓰레드2가 끝났다.

하지만 다른 작업의 작업량이나 시간이 현저히 적다면 우선 순위가 높다고 하더라도 더 늦게 끝날 수도 있다.

쓰레드 그룹

서로 관련이 있는 쓰레드들을 그룹으로 묶어서 다룰 수 있다.
관련이 있는 여러 쓰레드들을 일괄적으로 정지시키거나 실행시키는 등 묶어서 관리하고 싶을 때 사용한다.

  • 쓰레드들은 기본적으로 그룹에 포함되어 있다. JVM 이 시작되면 system 그룹이 생성되고 쓰레드들은 기본적으로 system 그룹에 포함된다.
  • 메인 쓰레드는 system 그룹 하위에 있는 main 그룹에 포함된다.
  • 모든 쓰레드들은 반드시 하나의 그룹에 포함되어 있어야 한다.
  • 쓰레드 그룹을 지정받지 못한 쓰레드는 자신을 생성한 부모 쓰레드의 그룹과 우선순위를 상속받게 되는데 보통 생성하는 쓰레드들은 main 쓰레드 하위에 분류된다.
  • 따라서 쓰레드 그룹을 지정하지 않으면 해당 쓰레드는 자동으로 main 그룹에 포함된다.

ThreadGroup이라는 클래스로 객체를 만들어 Thread를 생성할 때 new Thread(그룹, 작업, 쓰레드 이름) 이렇게 생성자에서 그룹을 지정해 사용한다.
ThreadGroup의 interrupt 메소드로 그룹 내의 일시 정지 상태인 쓰레드들을 실행 대기 상태로 만들 수 있다.

쓰레드 상태

상태
Enum
설명
객체생성
NEW
쓰레드 객체 생성, 아직 start() 메서드 호출 전의 상태
실행대기
RUNNABLE
실행 상태로 언제든지 갈 수 있는 상태
일시정지
WAITING
다른 쓰레드가 통지(notify) 할 때까지 기다리는 상태
일시정지
TIMED_WAITING
주어진 시간 동안 기다리는 상태
일시정지
BLOCKED
사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
종료
TERMINATED
쓰레드의 작업이 종료된 상태

오늘의 회고💬

비동기와는 조금 다른 병렬 처리 개념인 멀티 쓰레드에 대해 배웠다. 비동기 프로그래밍은 해본 적 있지만 멀티 쓰레드는 다뤄본 적이 없어서 개념적으로는 이해가 가는데 아직 자유자재로 구현하는 것은 어려울 것 같다.
예제로 학습하다 보면 자연스레 익숙해지긴 하겠지...

 

내일의 계획📜

개인 과제를 피드백에 맞춰 전반적으로 수정해야겠다.
아무래도 불필요하게 클래스를 나눈 것 같다. 너무 '절대 객체 지향을 구현해!!'에 꽂혀서 과하게 나눈 게 아닐까 생각한다😂

Comments