Java ‐ 멀티쓰레딩과 동시성 프로그래밍 - dnwls16071/Backend_Summary GitHub Wiki

📚 멀티쓰레딩이 필요한 근본적인 이유

왜 멀티쓰레딩이 필요한지?

  • 작업을 순서대로 처리하게 되면 하나가 끝날 때까지 다음 작업을 하지 못하게 된다.
  • 예제처럼 sleep(1000)으로 1초씩 대기하는 작업이 N개라면 총 N초가 소요된다.
  • 비효율적인 자원 사용 방식 - sleep() 동안 CPU는 작업을 중단하고 대기한다.
  • 이와 같이 비효율적인 자원 사용 방식을 타개하기 위해 동시에 여러 작업을 처리하고 싶을 때 멀티쓰레딩이 필요하다.

멀티쓰레딩

  • 하나의 프로그램에서 여러 작업(쓰레드)을 동시에 실행하는 것
  • 대기 시간이 많은 작업을 병렬로 처리할 수 있어 성능 향상 가능
    • CPU가 놀지 않고 다른 작업을 동시에 처리할 수 있다.
  • 특히 네트워크, 파일 I/O, UI 등에서 주로 사용된다.

멀티쓰레딩을 구현하는 방법

(1). Thread 클래스 상속

  • start() 메서드를 호출하면 새로운 스레드가 실행된다.
  • run()을 직접 호출하면 멀티 스레딩이 되지 않으므로 반드시 start()를 호출해야한다.
class MyThread extends Thread {
    private String name;
    
    public MyThread(String name) {
        this.name = name;
    }
 
    @Override
    public void run() {
        try {
            Thread.sleep(1000); // 1초 대기
            System.out.println(name + " 완료");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
    Thread t1 = new MyThread("작업 1");
    Thread t2 = new MyThread("작업 2");
    Thread t3 = new MyThread("작업 3");
 
    t1.start();
    t2.start();
    t3.start();
    }
}

(2). Runnable 인터페이스를 구현하는 방법

  • Thread 상속 방식보다 유연하며, 다른 클래스도 동시에 상속 가능
  • Thread를 상속받지 않고, Runnable 인터페이스를 구현
  • Thread 객체를 생성할 때 Runnable 객체를 전달하여 실행
class MyRunnable implements Runnable {
    private String name;
    
    public MyRunnable(String name) {
        this.name = name;
    }
 
    @Override
    public void run() {
        try {
            Thread.sleep(1000); // 1초 대기
            System.out.println(name + "완료");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable("작업 1"));
        Thread t2 = new Thread(new MyRunnable("작업 2"));
        Thread t3 = new Thread(new MyRunnable("작업 3"));
 
        t1.start();
        t2.start();
        t3.start();
    }
}

(3). Executors 프레임워크 사용

  • ExecutorService를 사용하여 스레드를 효율적으로 관리할 수 있다.
  • 스레드 개수를 자동으로 조절하며, 스레드 풀(Thread Pool) 기능을 제공한다.
  • 직접 Thread를 생성하는 것보다 성능과 안정성이 뛰어나다.
public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // 3개 스레드 사용
        Runnable task = () -> {
            try {
                Thread.sleep(1000); // 1초 대기
                System.out.println("스레드 실행: " + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
 
        for (int i = 0; i < 5; i++) {
            executor.execute(task); // 스레드 풀에서 스레드 실행
        }
        executor.shutdown(); // 스레드 풀 종료
    }
} 

❗쓰레드 풀은 미리 생성된 쓰레드들이 작업을 처리하는 방식이다. 쓰레드 풀은 쓰레드를 필요할 때마다 새로 생성하는 개념이 아니라 미리 준비된 쓰레드를 재사용함으로써 쓰레드 생성 및 종료에 드는 비용을 줄이고 성능을 최적화한다. ❗쓰레드 풀을 사용하면 한정된 수의 쓰레드로 여러 작업을 처리할 수 있다. 작업이 많아지면 대기 큐에 쌓이고 쓰레드 풀에 여유가 생기면 대기 중인 작업을 처리한다. 이를 통해 자원 관리를 효율적으로 할 수 있고, 쓰레드 수를 조절하여 시스템 리소스를 낭비하지 않게 된다.