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