백엔드 부트캠프/TIL

[내일배움캠프Spring-13일차] Thread

sintory-04 2025. 3. 6. 22:01

    1️⃣ 프로세스와 쓰레드

    - 프로세스: 실행중인 프로그램, 자원과 쓰레드로 구성(공장으로 비유)

    - 쓰레드: 프로세스 내에서 실제 작업을 수행, 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다. (일꾼으로 비유)

    " 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다 "

    2 프로세스 1 쓰레드 보다는 1프로세스 2쓰레드가 낫다고 말하는 것이다.(2개의 공장을 운영하는 거 보다 1개의 공장을 만드는 게 낫다는 것 !) => 싱글 쓰레드 프로세스가 두개인 것 보다 멀티 쓰레드 프로세스 1개, 쓰레드가 2개인게 낫다는 것이다.

    CGI 의 경우 웹서버에서 요청이 들어올 때 마다 하나의 프로세스를 만들었다. 하지만, Java Servlet 은 멀티 쓰레드를 지원해서 요청이 들어올 때마다 쓰레드만 생성해주면 되었다. 더 효율적이라는 것. (90년대 말)

    ✅ 멀티쓰레드의 장단점

    " 대부분의 프로그램이 멀티쓰레드로 작성되어 있다. "

    장점 시스템 자원을 보다 효율적으로 사용
    사용자에 대한 응답성이 향상
    작업이 분리되어 코드가 간결
    단점 동기화에 주의
    교착상태가 발생하지 않도록 주의 => 자원을 공유하다 보면, 문제가 생김.
    각 쓰레드가 효율적으로 고르게 실행되도록 하기 => 실행할 기회를 받지 못하고 기아 현상이 일어남.
    > 프로그래밍 시 고래해야할 사항이 많음.

    => 쪼금 쓰레드에 이해할려고 하자면, 카카오톡 사진 전송을 생각하면 된다 !

    - 싱글 쓰레드의 경우는 사진을 전송하게 되면, 채팅을 전송하지 못한다. 왜냐면 사진을 전송하기 있기 때문이다.

    - 멀티 쓰레드의 경우는 사진을 전송하면서, 채팅도 칠 수 있는 것이다. 이런게 멀티쓰레드의 장점이다.

    2️⃣ 쓰레드의 구현과 실행

    - run() 이라는 메서드를 구현하는 부분에서는 같음.

    - Thread 를 구현 하는 방법은 총 두가지이다.

    ① Thread 클래스를 상속

    class MyThread extends Thread {
    	public void run() { // Thread 클래스의 run() 을 오버라이팅
         /* 작업내용 */
        }
    }

    - 자바는 단일 상속만 가능하기 때문에 Thread를 상속 받으면 다른 클래스를 상속 받을 수 없다.

    > 실행방법

    MyThread t1 = new MyThread(); // 쓰레드의 생성
    t1.start(); // 쓰레드의 실행

    ② Runnable 인터페이스를 구현 (better)

    class MyThread2 inplements Runnable {
    	public void run() { // Runnable 인터페이스의 추상메서드 run() 을 구현
         /* 작업내용 */
        }
    }
    
    // 이거는Runnable interface 임
    public interface Runnable {
    	public abstract void run () ;
    }

    > 실행방법

    Runnable r = new MyThread2();
    Thread t2 = new Thread(r); // 이말은 Thread(Runnable r) 인거임 !
    // 위의 두 코드를 합치면 Thread t2 = new Thread(new MyThread2()); 로 줄일 수 있음.
    
    t2.start(); // 쓰레드의 실행

    3️⃣ 싱글 쓰레드와 멀티 쓰레드 구현

    1. 싱글 쓰레드

        public static void main(String[] args) {
            for (int i=0; i < 20 ; i++){
                System.out.println(0);
            }
            for (int i=0; i < 20 ; i++){
                System.out.println(1);
            }
        }

    - 반복문으로 숫자를 출력하는 Thread가 있다. 이와 같은 코드는 하나의 쓰레드로 싱글 쓰레드라고 불린다.

    - 싱글 쓰레드의 경우 하나의 작업이 끝난 후에 그 다음 작업이 실행되기 때문에 아래와 같은 출력문이 나오는 걸 확인 할 수 있다.

    OUTPUT
    0000000000000000000011111111111111111111
    Process finished with exit code 0

    - 0이 모두 출력 된 후에야 1 이 출력되는 걸 볼 수 있다. 

    2. 멀티 쓰레드

    class Main {
        public static void main(String[] args) {
            ThreadEx1_1 t1 = new ThreadEx1_1();
    
            Runnable r = new ThreadEx1_2();
            Thread t2 = new Thread(r);
    
            t1.start();
            t2.start();
        }
    }
    // Thread 를 상속 받은 쓰레드
    class ThreadEx1_1 extends Thread {
        public void run(){
            for (int i=0; i < 40 ; i++){
                System.out.print(0);
            }
        }
    }
    // Runnable 를 implements한 쓰레드
    class ThreadEx1_2 implements Runnable {
        public void run(){
            for (int i=0; i < 40 ; i++){
                System.out.print(1);
            }
        }
    }

    - 두 가지 방식으로 쓰레드를 만들었다.

    - 두 개의 쓰레드에게 각자 40개의 숫자를 출력하도록 했는데 출력문을 한 번 보자 !

    OUTPUT
    00000000001111111111111111111110000000000000011111111111111100000000000000001111
    Process finished with exit code 0

    - 그러면 이렇게 0과 1이 뒤섞여서 출력된 걸 볼 수 있다.

    - 확실히 싱글쓰레드와의 차이점이 보이지 않는가 ? 병렬적으로 쓰레드가 일하는 점이 차이점이다.

    - 쓰레드를 나누어 돌리면 번갈아가며 실행된다. Os스케줄러가 결정한 대로, 실행되는 것이기 때문에 먼저 start() 해도 먼저 실행할거라는 보장이 없다 !

    3. 멀티 쓰레드의 실행 - start()

    - Thread를 생성한 후에 start()를 호출해야지 쓰레드가 작업을 실행한다. 

    1. 메인 메서드에서 start 를 호출하면 아래와같은 그림이 된다.

    2. start 가 무엇을 하냐? 바로 새로운 호출스택을 생성한다.

    3. 새로운 호출 스택 부분에 run 을 올려주고 start는 사라진다.

    4. 그러면 각각의 호출 스택이 각각의 일을 독립적으로 처리 할 수 있는 것이다.

    - 왜 Tread를 불러 올 때 run() 이라는 함수를 작성했는데 start() 함수로 실행을 시켜야하는 것이지? 라는 생각을 할 수 있다.

    - 그냥 run()만 실행해버리면, 하나의 호출스택에 런을 넣어버리게 된다. 바로 아래의 사진과 같이 말이다. 

    이러면 싱글 쓰레드가 되기 때문에 멀티쓰레드를 만드는 의도와는 어긋나게 된다. 따라서, start()를 불러 새로운 호출 스택을 부른후 그 새로운 호출스택에 run을 올리는게 맞는 것이다 !

    4️⃣ 메인 쓰레드

    - 메인쓰레드란 ? 메인메서드 코드 수행하는 쓰레드

    - 쓰레드 종류 : 사용자 쓰레드, 데몬쓰레드(보조) 실행 중인 사용자 쓰레드가 없으면 프로그램 종료

    " 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다. " 

    - 이전에는 메인 메서드만 종료 되면 종료되었지만, 지금은 사용자 쓰레드가 없을 때 프로그램이 종료된다.

    1. 메인 쓰레드가 기다리지 않을 시

    class Exam13_11 {
      static long startTime = 0;
    
      public static void main(String[] args) {
        ThreadEx11_1 t1 = new ThreadEx11_1();
        ThreadEx11_2 t2 = new ThreadEx11_2();
        t1.start();
        t2.start();
        startTime = System.currentTimeMillis();
    
        System.out.println("\n 소요시간 : "+ (System.currentTimeMillis()-startTime));
      }
    }
    
    class ThreadEx11_1 extends Thread {
      public void run(){
        for (int i=0; i < 100 ; i++){
          System.out.print(0);
        }
      }
    }
    
    class ThreadEx11_2 extends Thread {
      public void run(){
        for (int i=0; i < 100 ; i++){
          System.out.print(1);
        }
      }
    }

    - 이렇게 될 시, 메인 쓰레드는 t1과 t2 보다 먼저 작업이 끝날 것이다. 왜냐면 ? t1과 t2를 실행만 시켜주면은 main 쓰레드의 작업은 끝나기 때문이다. 출력문을 보자

    OUTPUT
    00000000000011111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111111111111 소요시간 : 0

    - 소요시간이 0 인걸 볼 수 있다. 

    • 소요시간이 맨 뒤에 나오는 이유
      → main 스레드가 먼저 끝났지만, System.out.print(" 소요시간 : "+ (System.currentTimeMillis()-startTime)); 자체가 출력 버퍼에 남아 있다가 뒤늦게 출력될 가능성 때문
    • 왜 소요시간이 0일까?
      → main 스레드는 t1.start();와 t2.start();를 호출한 뒤 바로 startTime을 기록하고 print()를 실행하죠.
      → 그런데 t1과 t2가 실행되기 전에 System.currentTimeMillis() - startTime을 계산하면 시간이 거의 흐르지 않아서 0이 나오는 거이다.

    2. Join() 을 이용하여 메인 쓰레드 대기

    class Exam13_11 {
      static long startTime = 0;
    
      public static void main(String[] args) {
        ThreadEx11_1 t1 = new ThreadEx11_1();
        ThreadEx11_2 t2 = new ThreadEx11_2();
        t1.start();
        t2.start();
        startTime = System.currentTimeMillis();
    
        try {
          t1.join();
          t2.join();
        } catch (InterruptedException e) {}
    
        System.out.print(" 소요시간 : "+ (System.currentTimeMillis()-startTime));
      }
    }
    
    class ThreadEx11_1 extends Thread {
      public void run(){
        for (int i=0; i < 100 ; i++){
          System.out.print(0);
        }
      }
    }
    
    class ThreadEx11_2 extends Thread {
      public void run(){
        for (int i=0; i < 100 ; i++){
          System.out.print(1);
        }
      }
    }

     

    - 이렇게 될 경우는 Join메서드를 통해 다른 쓰레드 작업 끝날때까지 기다린다.

    OUTPUT
    00000000000000000000011111111111111100000000000000000000011111110000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000 소요시간 : 1

    - 소요시간도 달라진 것을 볼 수 있다 ! 메인 쓰레드의 소요시간이 늘어났다는 건 t1과 t2의 작업이 끝난 후 메인 쓰레드가 끝났다는 걸 의미한다.

    5️⃣ 싱글쓰레드와 멀티쓰레드의 시간 차이

    1. 싱글쓰레드

    - A 작업이 끝난 후 B 작업이 끝난다.

    2. 멀티 쓰레드

    - 두 작업을 번갈아가면서 실행한다. A 작업을 실행 후 B 작업으로 이동하는 시간(context Switching 시간소요)이 있기 때문에 어쩔 수 없이 싱글쓰레드보다 시간이 더 긴 걸 볼 수 있다.

    - 그렇다면 시간이 더 걸리는데 왜 멀티 쓰레드를 사용하는 것인가? 할 수 있다. 하지만 ! 시간이 좀 더 걸리더라도 여러 작업을 동시에 할 수 있으며 작업을 좀 더 효율적으로 할 수 있다 ex) 채팅프로그램, 카톡

    6️⃣ 쓰레드의 I/O 블락킹 IO Blocking

    (IO 는 입력 출력을 말한다)

    1. 싱글 쓰레드

     -  싱글쓰레드일 경우 사용자가 입력을 하지 않을 시, 입력을 받을 때까지 아무 일도 하지 않는다.

    - 코드를 보겠다 ! 메인 창에 반복문을 통해 카운트다운을 하고 있다.

    public static void main(String[] args) {
        String input = JOptionPane.showInputDialog("입력하세요");
        System.out.println("입력하신 값은" + input + "입니다.");
    
        for (int i=10; i > 0; i--){
          System.out.println(i);
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
       }
    }

    - 출력문을 보니, input 값을 입력하지 않으면 아래 출력문이 없는 것을 볼 수 있다. 그런데 입력을 하니 정상적으로 카운트가 되는 걸 확인할 수 있다.

    2. 멀티쓰레드 

    - 멀티쓰레드는 작업 수행동안 외부 요인 탓에 멈춰지더라도 다른 쓰레드가 작업 진행이 된다.

    import javax.swing.*;
    
    public class Ex13_5 {
      public static void main(String[] args) {
        ThreadEx5_1 th1 = new ThreadEx5_1();
        th1.start();
    
        String input = JOptionPane.showInputDialog("입력하세요");
        System.out.println("입력하신 값은" + input + "입니다.");
      }
    }
    
    class ThreadEx5_1 extends Thread {
      public void run() {
        for (int i=10; i > 0; i--){
          System.out.println(i);
          try {
            sleep(1000);
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
        }
      }
    }

    - 메인 창에 쓰레드를 불러와서 카운트다운을 하고 있다. 출력하여 보니, 입력을 하지 않아도 아래의 출력문이 잘 나오는 걸 확인 할 수 있다.

    - 효율적인 작업이 가능하며, 싱글 쓰레드 보다 빨리 끝난다는 장점이 있다는 걸 알 수 있다. 

    7️⃣ 최종 정리

    구분 싱글 쓰레드 (Single Thread) 멀티 쓰레드 (Multi Thread)
    실행 방식 하나의 작업을 순차적으로 실행 여러 작업을 동시에 실행
    CPU 활용 하나의 작업만 실행하므로 CPU 활용도가 낮음 여러 작업을 동시에 실행하여 CPU 활용도가 높음
    속도 느림 (작업이 끝나야 다음 작업 실행) 빠름 (여러 작업을 동시에 처리)
    메모리 사용 적게 사용 더 많은 메모리 사용
    코드 구현 난이도 상대적으로 쉬움 동기화, 경합 조건 등 고려해야 하므로 어려움
    디버깅 난이도 쉬움 (순차 실행이라 흐름이 명확함) 어려움 (실행 순서가 달라질 수 있음)
    응답성 하나의 작업이 오래 걸리면 프로그램이 멈춘 것처럼 보일 수 있음 다른 작업이 병렬로 실행되므로 응답성이 좋음

    ✅ 오늘의 회고

    오늘은 기초 알고리즘 문제도 7개를 풀어서 포스팅을 했다. (1,2,3,4,5,6,7)

    그리고 드디어 계산기 3 레벨을 구현해서 깃허브에 올렸고, Readme를 작성해서 정리해두었다. readme 내용은 그대로 블로그에도 포스팅 했다 ! 

    계산기를 구현하다가 끝나니 조금 나태해진 거 같아서 따로 Tread 강의를 보았다. 완전히 와 닿지 않아서 유튜브에 있는 자바의 정석 강의를 보았다. 근데 웬걸.. 너무 이해가 잘된다. 나름 Thread 부분이 재미있다고 까지 생각되니 내가 미쳤나보당 🫠

    막상 응용하는 부분이 나온다면,, 엄청 어려울 거 같다 😱