Java

Thread

dev-rootable 2024. 5. 3. 11:38

📌 Thread란?

 

프로세스를 구성하는 세부 실행단위

 

자바는 JVM을 통해 멀티스레드를 구성하여 Concurrent 하게 동작하여 동시에 여러 작업을 수행할 수 있다. 하지만 동시에 실행하는 Thread의 수행 순서는 보장되지 않는다.

 

Concurrent vs Parallel (동시성 vs 병렬성)

두 성질 모두 여러 작업을 동시에 처리하는 멀티스레드 동작을 말한다.

Concurrent는 여러 Thread가 물리적으로 동시에 실행되는 것이 아닌, 실제 동작은 여러 Thread의 수행 시간을 쪼개어
동작
한다. 즉, 번갈아 수행한다는 의미다.

Parallel은 Concurrent와 달리 물리적으로 동시에 여러 Thread를 병렬적으로 수행한다. 즉, 동시에 수행한다는
의미다.

 

JVM이 가장 먼저 수행하는 Thread는 Main Thread다. 따라서 메인 Thread와 다른 Thread의 종료는 별개로 동작한다. 메인 Thread가 종료되어도 다른 모든 Thread가 종료되어야 JVM의 동작이 멈춘다.

 

🧪 Multi-Thread 구현

 

1. Thread 클래스 상속

 

public class MyThread extends Thread {
    @Override
    public void run() {
        // 스레드가 실행할 코드 작성
    }
}

 

 

Thread를 상속받은 MyThread는 run 메서드를 오버라이딩하여 Thread가 실행할 코드를 작성할 수 있다. Thread를 실행하려면 start 메서드를 호출해야 한다.

 

MyThread myThread = new MyThread();
myThread.start();

 

 

2. Runnable 인터페이스 구현

 

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 스레드가 실행할 코드 작성
    }
}

 

Runnable를 구현한 MyRunnable는 run 메서드를 오버라이딩하여 Thread가 실행할 코드를 작성할 수 있다. Thread를
실행
하려면 start 메서드를 호출해야 한다.

 

MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();

 

📝 주요 메서드

 

✔ start()

 

start 메서드를 실행하면 native 메서드인 start0()를 통해 Thread를 스케줄러에 포함시킬 수 있다.

 

스케줄러에 포함된 Thread는 Thread Q 내에서 runnable로 대기하다가 수행 차례가 되면 run() 메서드를 호출하여
동작을 수행
한다.

 

Thread는 단 한 번만 start()가 호출될 수 있으며, Terminated 되었거나 Waiting인 상태여도 start()를 호출할 수
없다.

 

✔ run()

 

start()를 호출하면 스케줄러 내에서 실제 동작하는 부분이다. Thread를 상속받거나 Runnable을 구현할 때, run()을
오버라이딩해야 한다.

 

Thread의 start()를 호출하지 않고, run()을 직접 호출하면 Thread는 스케줄러에 포함되지 않고 일반 메서드를 호출한 것처럼 동작한다. 그렇기 때문에 run()을 수행하는 Thread는 호출한 Thread가 된다.

 

✔ sleep()

 

Thread를 Waiting Pool로 보내 대기시킬 수 있다.

 

static 메서드이므로 호출한 Thread가 대기 상태에 들어간다.

 

ms 단위의 시간을 매개변수로 받아 해당 시간만큼 Thread를 대기시킨다.

 

인터럽트가 발생하면 다시 Runnable 상태가 되며 InterruptedException을 발생시킨다.

 

public static native void sleep(long millis) throws InterruptedException;

 

📖 Thread 생명주기

 

https://codedragon.tistory.com/3526

 

🔸 New(객체 생성)

 

  • Thread가 만들어진 상태
  • start 메서드가 호출되지 않은 상태
  • new를 통해 객체를 생성한 상태

 

🔸 Runnable(실행 대기)

 

  • 실행 상태로 언제든지 갈 수 있는 상태
  • 객체가 생성된 후 start 메서드를 호출한 상태

 

🔸 Running(실행)

 

  • Runnable 상태에서 Thread 스케줄러에 의해 실행 상태로 이동하게 된다.
  • Runnable 상태가 아닌 Thread는 Running 상태가 될 수 없다.
  • 스케줄러는 Runnable 상태의 Thread 중 하나를 선택해서 실행하게 된다.

 

🔸 Blocked(봉쇄)

 

  • 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태
  • sleep(), join() 메서드를 호출한 상태

 

🔸 Waiting(대기)

 

  • 다른 Thread가 통지할 때까지 기다리는 상태

 

🔸 Timed_waiting(시간 대기)

 

  • 주어진 시간 동안 기다리는 상태

 

🔸 Terminated/Dead(종료)

 

  • 실행을 마친 상태
  • run 메서드를 완료한 상태
  • 한번 종료된 Thread는 다시 Runnable 상태가 될 수 없다.

 

💥 동기화 문제

 

여러 Thread가 동시에 하나의 변수에 접근하면 동기화 문제가 발생할 수 있다.

 

자바는 Synchronized 기능이나 Lock 인터페이스를 사용하여 Thread 간의 접근을 조절하여 해결한다.

 

🔍 Synchronized 키워드

 

   public class Counter {
        private int count = 0;

        public synchronized void increment() {
            count++;
        }

        public synchronized int getCount() {
            return count;
        }
    }

 

synchronized 키워드가 붙은 메서드 중 하나가 실행 중일 때, 다른 Thread가 해당 메서드를 실행할 수 없도록 하여 안전하게 변수나 자원을 처리할 수 있게 된다.

 

🚘 주의

동기화된 메서드는 하나의 Thread만 실행할 수 있어 실행 완료까지 다른 Thread는 대기해야 한다. 그래서 키워드를
남용하게 되면 대기로 인한 CPU 코어 간의 경쟁이 증가하여 성능 문제가 발생할 수 있다. 따라서 동기화가 필요한
부분에만 사용하고 남용하지 않도록 유의해야 한다.

 

🔍 Lock 인터페이스

 

자바에서 제공하는 동기화 기능을 구현하는 인터페이스

 

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Counter {
        private int count = 0;
        private Lock lock = new ReentrantLock();
    
        public void increment() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    
        public void decrement() {
            lock.lock();
            try {
                count--;
            } finally {
                lock.unlock();
            }
        }
    
        public int getCount() {
            return count;
        }
    }

 

lock 메서드로 락을 얻고 unlock 메서드로 락을 반환하도록 구현되어 있다. 만약 락이 이미 다른 Thread에 의해 획득되어
있다면, 해당 Thread는 락을 획득할 때까지 대기하게 된다.

 

📚 Thread Pool

 

미리 일정 개수의 Thread를 생성하여 관리하는 기법

 

https://www.baeldung.com/thread-pool-java-and-guava

 

1. 미리 생성된 Thread들은 작업을 할당받기 위해 대기

2. 작업이 발생하면 대기 중인 Thread 중 하나를 선택하여 작업을 수행

3. 작업이 완료되면 해당 Thread는 다시 대기 상태로 돌아가고, 새로운 작업을 할당받을 준비

 

❓ 장점

 

✅ 자원 효율성

 

  • Thread Pool을 사용하면 작업마다 Thread를 생성 및 삭제로 인한 오버헤드를 줄일 수 있다.
  • 특정 시점에 동시에 처리할 수 있는 작업의 개수를 제한할 수 있어 시스템의 자원을 효율적으로 관리하고 성능을 향상할 수 있다.

 

✅ 응답성 및 처리량 향상

 

  • Thread Pool은 작업을 대기 상태로 유지하여 작업 처리 속도를 향상할 수 있다.
  • 즉, 작업이 발생하면 대기 중인 Thread 중 하나를 선택하여 작업을 할당하므로, 작업 처리를 병렬로 진행할 수 있다.

 

✅ 작업 제어

 

  • 동시에 처리할 수 있는 작업의 개수를 제한할 수 있다.
  • 즉, Thread Pool의 크기를 조절하여 시스템의 부하를 조절하고, 과도한 작업 요청으로 인한 성능 저하를 방지할 수 있다.

 

✅ Thread 관리

 

  • Thread의 생명 주기를 관리할 수 있다.
  • Thread Pool은 Thread의 생성, 재사용, 종료 등을 관리하므로 Thread의 안전한 운영을 돕는다.

 

🖐 Thread Pool에 부적합한 애플리케이션

 

🔸 실행 시간이 오래 걸리는 작업과 금방 끝나는 작업을 섞어서 실행하는 케이스

 

🔸 다른 작업의 내용에 의존성을 갖고 있는 작업

 

일반적인 네트워크 기반의 서버 애플리케이션(웹서버, 메일서버, 파일서버 등)작업이 서로 동일하면서 독립적이어야 한다는 조건을 대부분 만족하므로 Thread Pool에 적합하다.

 

🔨 사용 방법

 

Executor 인터페이스와 그 하위 인터페이스들을 통해 사용할 수 있다.

 

// 쓰레드 풀 생성
ExecutorService executor = Executors.newFixedThreadPool(5);
    
// 작업 할당
for (int i = 0; i < 10; i++) {
   executor.submit(new MyRunnableTask(i));
}
    
// 쓰레드 풀 종료
executor.shutdown();

 

ExecutorService 인터페이스는 Thread Pool을 생성하고 관리하기 위한 메서드들을 제공한다.

 

코드를 보면 Executors.newFixedThreadPool(5)는 5개의 Thread를 가지는 Thread Pool을 생성한다. 그리고 executor.submit(new MyRunnableTask(i))는 MyRunnableTask라는 Runnable 구현체를 생성하고, Thread Pool에 작업을 할당한다.

 

References:

 

https://velog.io/@daelkdev/Java-java%EB%8A%94-%EB%A9%80%ED%8B%B0%EC%93%B0%EB%A0%88%EB%93%9C%EB%A5%BC-%EC%96%B4%EB%96%A4-%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EC%A7%80%EC%9B%90%ED%95%98%EB%82%98%EC%9A%94

 

[Java] java는 멀티쓰레드를 어떤 식으로 지원하나요?

멀티쓰레드를 지원함으로써, 하나의 자바 프로그램에서 여러 개의 쓰레드를 동시에 실행할 수 있습니다.

velog.io

 

https://velog.io/@tomato2532/JAVA-Thread-1-%EC%9E%90%EB%B0%94%EC%9D%98-%EC%93%B0%EB%A0%88%EB%93%9C#start

 

[JAVA] Thread 1 - 자바의 쓰레드

자바의 쓰레드는 Concurrent하게 동작합니다. Concurrent 는 논리적으로 여러 작업을 동시에 처리하는 멀티쓰레드 동작이지만 여러 쓰레드가 물리적으로 동시에 실행되지는 않습니다. 실제 동작은 여

velog.io

 

https://engineerinsight.tistory.com/197

 

[JAVA] 쓰레드 풀(Thread Pool): 개념, 장점, 사용 방법, 코드 예시 (feat. Baeldung)

💋 쓰레드 풀의 필요성 서버는 동시에 여러 사용자가 접속할 수 있습니다. 자바에서는 스레드를 운영 체제의 자원으로 사용합니다. 우리가 스레드를 계속해서 만들면, 운영 체제의 자원이 빨리

engineerinsight.tistory.com

 

https://codedragon.tistory.com/3526

 

Thread life cycle(스레드의 생명주기)

스레드의 생명주기스레드는 Thread 객체가 생성되면 생명주기를 갖게 되는데 크게 5가지로 나누게 됩니다.

codedragon.tistory.com