[JAVA] 쓰레드(Thread)

이연우·2025년 7월 9일

TIL

목록 보기
7/100

🧵 쓰레드란?

  • 프로세스 내에서 실행되는 독립적인 작업 흐름 단위
    → 하나의 자바 애플리케이션(JVM)은 하나 이상의 쓰레드를 가질 수 있음
    → 기본적으로 자바 프로그램은 Main 쓰레드에서 실행됨
    → 코드를 작성하면서 명시적으로 생성하는 쓰레드는 Main 이외의 추가 작업 흐름

📊 쓰레드와 프로세스 비교

항목프로세스쓰레드
정의실행 중인 프로그램프로세스 내의 실행 흐름
메모리독립적 공간 사용메모리 공유 (heap, static 등)
생성 비용높음낮음
통신IPC 필요공유 변수 사용 가능
자바에서JVM 단위Thread 클래스

➡️ 싱글 쓰레드(Single Thread)

  • 하나의 쓰레드만 사용하는 프로그램

→ 프로그램 전체가 한 작업 흐름에서 차례대로 실행됨흐름에서 차례대로 실행됨
Main 쓰레드 하나만 존재
→ 작업이 순차적으로 처리됨(이전 작업이 끝나야 다음 작업 진행)

🗒️ 예시 코드:

public class Main {

    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작 :::");
        String threadName = Thread.currentThread().getName();
        
        // ✅ 하나의 작업 단위: 숫자를 0 부터 9 까지 출력
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 0.5 초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // ✅ 추가작업
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 0.5 초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("::: 작업 끝 :::");
    }
}

🔀 멀티 쓰레드(Multi-Thread)

  • 여러 개의 쓰레드가 동시에 실행되는 프로그램

→ 여러 작업을 병렬(또는 동시)적으로 수행 가능
→ CPU 코어가 여러 개면 진짜 병렬, 하나면 빠른 전환
→ 작업이 서로 영향을 주지 않도록 동기화가 필요할 수 있음

🗒️ 예시 코드:

// ✅ Thread 클래스 상속으로 쓰레드 구현
public class MyThread extends Thread {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("::: " + threadName + "쓰레드 시작 :::");
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 딜레이 0.5 초
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("::: " + threadName + "쓰레드 종료 :::");
    }
}
public class Main {

    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작");

        MyThread thread0 = new MyThread();
        MyThread thread1 = new MyThread();

        // 1. thread0 실행
        System.out.println("::: main 이 thread0 을 실행");
        thread0.start();

        // 2. thread1 실행
        System.out.println("::: main 이 thread1 을 실행");
        thread1.start();

        System.out.println("::: main 쓰레드 종료");
    }
}

📊 싱글 vs 멀티 쓰레드 비교

항목싱글 쓰레드멀티 쓰레드
실행 흐름하나여러 개
속도느릴 수 있음병렬 실행으로 더 빠름
구현간단상대적으로 복잡
자원 소비적음많음 (관리 필요)
동시 작업불가능가능
예외/버그 발생적음동기화 안 되면 위험 가능성 있음

🔗 join()이란?

  • 다른 쓰레드가 종료될 때까지 현재 쓰레드를 일시 정지시키는 메서드
  • A.join() → 현재 쓰레드는 A 쓰레드가 끝날 때까지 기다림
  • join()을 호출하지 않으면 쓰레드들이 병렬로 실행되어 끝나는 순서 예측 불가
  • 예외: InterruptedException을 반드시 처리해야 함

🗒️ 예시 코드:

public class Main {

    public static void main(String[] args) {
        System.out.println("::: main 쓰레드 시작");
        MyThread thread0 = new MyThread();
        MyThread thread1 = new MyThread();

        // 시작시간 기록
        long startTime = System.currentTimeMillis();

        // 1. thread0 시작
        System.out.println("thread0 시작");
        thread0.start();

        // 2. thread1 시작
        System.out.println("thread1 시작");
        thread1.start();

        // ⌛️ main 쓰레드 대기 시키기
        try {
            thread0.join();
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("총 작업 시간: " + totalTime + "ms");
        System.out.println("::: main 쓰레드 종료");
    }
}

🧩 Runnable 인터페이스란?

  • 쓰레드에서 실행할 코드를 정의하는 함수형 인터페이스
  • 즉, run() 메서드 하나만 가진 인터페이스로, 쓰레드에 넘겨서드 하나만 가진 인터페이스로, 쓰레드에 넘겨줄 수 있음
@FunctionalInterface
public interface Runnable {
    void run();
}

→ 자바의 Thread 클래스는 Runnable을 매개변수로 받을 수 있음
→ 람다식으로도 구현 가능(함수형 인터페이스이기 때문)

1️⃣ 유지 보수성과 재사용성 향상

  • Thread는 쓰레드를 제어하기 위해 존재하는 클래스
  • Thread 클래스를 상속받아 MyThread를 구현하면 실행 로직쓰레드 제어 로직이 결합되어 한 가지 클래스에서 두 가지 역할을 담당하게 됨
    → 실행 로직: 숫자 0~9 출력
    → 쓰레드 제어 로직: start(), join(), isAlive()
    → 하나의 클래스는 하나의 책임만 가지는 것이 유지 보수에 좋음
  • Runnable을 활용하면 실행 로직을 별도의 구현체로 분리할 수 있음
    Thread: 쓰레드 제어
    Runnable 구현체: 실행 로직 관리

🗒️ 예시 코드:

// ⚠️ Thread는 Thread 제어 역할 그리고 실행로직 두가지를 담당합니다.
public class MyThread extends Thread {

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("현재 시작된 쓰레드: " + threadName);

        for(int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드 : " + threadName + " - " + i);
            try {
                Thread.sleep(500); // 딜레이 0.5 초
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("종료된 쓰레드: " + threadName);
    }
}
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {

    public static void main(String[] args) {

        MyRunnable task = new MyRunnable(); // ✅ 하나의 작업 객체 선언

				// ✅ 하나의 작업을 여러 쓰레드에서 공유
        Thread thread0 = new Thread(task); // 작업 객체 공유
        Thread thread1 = new Thread(task); // 작업 객체 공유

				// 실행
        thread0.start();
        thread1.start();
    }
}

2️⃣ 확장 가능성

  • _Thread를 상속해서 MyThread를 구현하면 다른 클래스를 상속받지 못함
    → JAVA는 클래스의 다중 상속을 지원하지 않음
    Thread_를 상속하면 다른 클래스를 상속할 수 없어 확장성이 떨어짐
  • Runnable은 인터페이스이므로 기존 클래스의 기능을 유지하면서 상속을 통해 확장할 수 있음

🗒️ 예시 코드:

public class MyNewClass { // ✅ 새로운 클래스 

    public void printMessage() {
        System.out.println("MyClass 기능 실행");
    }
}
public class MyThread extends Thread, MyNewClass{ // ❌ 다중 상속 불가
		...
}
public class MyRunnable extends MyNewClass implements Runnable { // ✅ 다중 상속

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 10; i++) {
            System.out.println("현재 쓰레드: " + threadName + " - " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Main {

    public static void main(String[] args) {

        MyRunnable task = new MyRunnable();

        // ✅ 기존 클래스를 유지하면서 확장해서 활용
        task.printMessage(); 

        Thread thread0 = new Thread(task);
        Thread thread1 = new Thread(task);

        thread0.start();
        thread1.start();
    }
}

✏️ 연습 문제) 싱글 VS 멀티 쓰레드 - 숫자 합계 시간 비교

  • 1부터 1000까지의 숫자 합계 구하기
    → 연산 사이에 10ms 딜레이를 추가하여 0.01초마다 계산 - Thread.sleep(10); 예시
  • 싱글 쓰레드는 하나의 루프로 계산
  • 멀티 쓰레드는 4개의 쓰레드가 각각 숫자를 나누어 계산
    → 1~250
    → 251~500
    → 501~750
    → 751~1000

- SingleThread.java

public class SingleThread {
   public long calculateSum() {
       long start = System.currentTimeMillis();  // 시작 시간 기록

       int sum = 0;
       for (int i = 1; i <= 1000; i++) {
           sum += i;
           try {
               Thread.sleep(10); // 0.01 초 대기
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }

       long end = System.currentTimeMillis();  // 종료 시간 기록
       System.out.println("싱글 쓰레드 소요 시간: " + (end - start) + "ms");

       return sum;
   }
}

- MultiThread.java

public class MultiThread implements Runnable {
   private int start;
   private int end;
   private long sum;

   public MultiThread(int start, int end) {
       this.start = start;
       this.end = end;
   }

   public long getSum() {
       return this.sum;
   }

   @Override
   public void run() {
       for (int i = start; i <= end; i++) {
           this.sum += i;
           try {
               Thread.sleep(10);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   }
}

- Main.java

package chapter3_7;

public class Main {
   public static void main(String[] args) {
       System.out.println("::: main 쓰레드 시작");

       // 싱글 쓰레드
       SingleThread s1 = new SingleThread();
       long single_result = s1.calculateSum();
       System.out.println("싱글 쓰레드 결과: " + single_result);

       // 멀티 쓰레드
       long multiStartTime = System.currentTimeMillis();

       MultiThread result0 = new MultiThread(1, 250);
       MultiThread result1 = new MultiThread(251, 500);
       MultiThread result2 = new MultiThread(501, 750);
       MultiThread result3 = new MultiThread(751, 1000);

       Thread thread0 = new Thread(result0);
       Thread thread1 = new Thread(result1);
       Thread thread2 = new Thread(result2);
       Thread thread3 = new Thread(result3);

       thread0.start();
       thread1.start();
       thread2.start();
       thread3.start();

       try {
           thread0.join();
           thread1.join();
           thread2.join();
           thread3.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       long sum0 = result0.getSum();
       long sum1 = result1.getSum();
       long sum2 = result2.getSum();
       long sum3 = result3.getSum();

       long multi_total =  sum0 + sum1 + sum2 + sum3;

       long multiEndTime = System.currentTimeMillis();
       long multiDuration = multiEndTime - multiStartTime;

       System.out.println();
       System.out.println("멀티 쓰레드 소요시간: " + multiDuration + "ms");
       System.out.println("멀티 쓰레드 결과: " + multi_total);

       System.out.println("::: main 쓰레드 종료");
   }
}

출력 결과

::: main 쓰레드 시작
싱글 쓰레드 소요 시간: 10744ms
싱글 쓰레드 결과: 500500

멀티 쓰레드 소요시간: 2715ms
멀티 쓰레드 결과: 500500
::: main 쓰레드 종료

0개의 댓글