1️⃣ 프로세스와 쓰레드
- 프로세스: 실행중인 프로그램, 자원과 쓰레드로 구성(공장으로 비유)
- 쓰레드: 프로세스 내에서 실제 작업을 수행, 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다. (일꾼으로 비유)
" 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다 "
2 프로세스 1 쓰레드 보다는 1프로세스 2쓰레드가 낫다고 말하는 것이다.(2개의 공장을 운영하는 거 보다 1개의 공장을 만드는 게 낫다는 것 !) => 싱글 쓰레드 프로세스가 두개인 것 보다 멀티 쓰레드 프로세스 1개, 쓰레드가 2개인게 낫다는 것이다.
CGI 의 경우 웹서버에서 요청이 들어올 때 마다 하나의 프로세스를 만들었다. 하지만, Java Servlet 은 멀티 쓰레드를 지원해서 요청이 들어올 때마다 쓰레드만 생성해주면 되었다. 더 효율적이라는 것. (90년대 말)
✅ 멀티쓰레드의 장단점
" 대부분의 프로그램이 멀티쓰레드로 작성되어 있다. "
장점 | 시스템 자원을 보다 효율적으로 사용 사용자에 대한 응답성이 향상 작업이 분리되어 코드가 간결 |
단점 | 동기화에 주의 교착상태가 발생하지 않도록 주의 => 자원을 공유하다 보면, 문제가 생김. 각 쓰레드가 효율적으로 고르게 실행되도록 하기 => 실행할 기회를 받지 못하고 기아 현상이 일어남. > 프로그래밍 시 고래해야할 사항이 많음. |
=> 쪼금 쓰레드에 이해할려고 하자면, 카카오톡 사진 전송을 생각하면 된다 !
- 싱글 쓰레드의 경우는 사진을 전송하게 되면, 채팅을 전송하지 못한다. 왜냐면 사진을 전송하기 있기 때문이다.
- 멀티 쓰레드의 경우는 사진을 전송하면서, 채팅도 칠 수 있는 것이다. 이런게 멀티쓰레드의 장점이다.
2️⃣ 쓰레드의 구현과 실행
- run() 이라는 메서드를 구현하는 부분에서는 같음.
① 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가 있다. 이와 같은 코드는 하나의 쓰레드로 싱글 쓰레드라고 불린다.
- 싱글 쓰레드의 경우 하나의 작업이 끝난 후에 그 다음 작업이 실행되기 때문에 아래와 같은 출력문이 나오는 걸 확인 할 수 있다.
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개의 숫자를 출력하도록 했는데 출력문을 한 번 보자 !
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 쓰레드의 작업은 끝나기 때문이다. 출력문을 보자
- 소요시간이 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메서드를 통해 다른 쓰레드 작업 끝날때까지 기다린다.
- 소요시간도 달라진 것을 볼 수 있다 ! 메인 쓰레드의 소요시간이 늘어났다는 건 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 부분이 재미있다고 까지 생각되니 내가 미쳤나보당 🫠
막상 응용하는 부분이 나온다면,, 엄청 어려울 거 같다 😱
'백엔드 부트캠프 > TIL' 카테고리의 다른 글
[내일배움캠프Spring-15일차] Kiosk Issue (2) | 2025.03.10 |
---|---|
[내일배움캠프Spring-14일차] 키오스크 문제 (0) | 2025.03.07 |
[내일배움캠프Spring-12일차] Enum (1) | 2025.03.05 |
[내일배움캠프Spring-11일차] DFS 이해하기 (1) | 2025.03.04 |
[내일배움캠프Spring-10일차] 계산기 과제 Issue (1) | 2025.02.28 |