[Java] Thread

Jeini·2023년 3월 14일
1

☕️  Java

목록 보기
54/70
post-thumbnail

❗️ Java 멀티 쓰레드 프로그래밍을 익히고 Server에 어떻게 사용되는지 이해해보자


💡 병렬화 할 때 고려해야 할 것들

  • 메모리의 속도
  • CPU 캐시 메모리
  • 디스크
  • 네트워크
  • 컨넥션

❗️ 순차적 실행이 병렬실행 보다 빠른 경우도 있다. 동시 실행에 따르는 오버헤드가 없고, 단일 CPU 알고리즘은 하드웨어 작업에 더 친화적일 수 있기 때문이다.


💡 프로세스(Process)

✔️ 단순히 실행중인 프로그램

  • 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 의미.
  • 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성됨.


📎 프로세스 특징

  • 각각의 프로세스는 메모리 공간에서 독립적으로 존재한다.
  • 각각의 프로세스는 위 그림과 같이 자신만의 메모리 구조를 가진다.
  • 프로세스 A, B, C가 있을 경우 각각 프로세스는 모두 같은 구조의 메모리 공간을 가진다.
  • 독립적인 만큼 다른 프로세스의 메모리 공간에 접근할 수 없다.

✏️ Mac 활성상태보기에서 프로세스 보기

각각의 프로세스들은 자신만의 메모리 영역을 확보한 상태로 실행되고 있다는 것을 알 수 있다.


🔗 IPC

✔️ 프로세스 간의 통신하는 방법

  • 프로세스끼리는 독립된 메모리 공간 때문에 바로 데이터를 주고받을 수 없다.
  • 그래서 OS가 제공하는 공식적인 통신 수단을 사용한다.
  • 프로세스 A에서 프로세스 B를 직접 접근할 수 없기 때문에 프로세스 간의 통신을 하는 특별한 방식이 필요하다. ➡️ 메일슬록(mailslot), 파이프(pipe)
  • 프로세스는 독립적인 메모리 공간을 지니기 때문에 IP를 통하지 않고 통신할 수 없다.
  • 프로세스가 여럿이 병렬적으로 실행되기 위해서는 필연적으로 컨텍스트 스위칭이 발생할 수 밖에 없다. 그것을 해결할 수 있는 것이 Thread이다.

✏️ 예시 비유:

  • 프로세스 = 서로 다른 회사
  • IPC = 회사끼리 자료 보내고 받는 공식 이메일 / 회의 / 우편

✅ IPC 종류

종류설명Java에서 쓰는 방법
파이프(Pipe)한 프로세스 출력 → 다른 프로세스 입력PipedInputStream, PipedOutputStream
메시지 큐(Message Queue)메시지를 큐에 넣고 읽음JMS, RabbitMQ 등
공유 메모리(Shared Memory)OS가 특정 메모리를 여러 프로세스가 공유Java는 직접 불가, JNI 통해 가능
소켓(Socket)네트워크 기반 IPCSocket, ServerSocket
신호(Signal)이벤트 발생 알림OS 신호 (Java는 제한적)

🧵 스레드(Thread)

✔️ 실행 단위

  • 즉, 프로세스 내에서 실제로 작업을 수행하는 주체를 의미
  • 쉽게 말하면, 프로그램 안에서 동시에 실행되는 작은 흐름이다.

✏️ 예시 비유:

  • 프로그램(프로세스) = 회사
  • 스레드 = 회사 안에서 일하는 직원

👉 직원이 각자 일을 처리하면서 동시에 돌아감

❗️ 특정시간에 Thread1 특정시간에 Thread2 이렇게 시간을 쪼개서 각각의 스레드가 실행됐다 안됐다를 반복하면서 실행하게 된다.

1. 같은 메모리 공간 공유

  • 하나의 프로세스 안의 스레드들은 변수, 객체, 힙 메모리를 공유

2. 독립적인 실행 흐름

  • 각 스레드는 자신의 스택과 레지스터를 가지고 독립적으로 실행됨

3. 병렬 처리 가능

  • CPU가 멀티코어면 진짜 동시에 실행됨
  • 싱글코어면 시분할로 순서 빠르게 바꿔서 동시에 돌아가는 것처럼 보임

✏️ Java에서 스레드 예시

class MyThread extends Thread {
    public void run() {
        for(int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 실행: " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        t1.start();
        t2.start();
    }
}

✏️ 메모리 공간에서의 스레드

  • 프로세스와 프로세스의 전환보다, 스레드와 스레드가 전환하는 비용이 더 적다.

💡 스레드의 상태(state of thread)

Java에서 스레드는 생명주기(lifecycle)를 가지고 있고, 각 상태를 이해하면 디버깅이나 설계할 때 도움이 된다.

상태(State)설명예시
NEW스레드 객체가 생성되었지만, 아직 start() 안 한 상태Thread t = new Thread();
RUNNABLEstart()가 호출되어 실행 대기 중 또는 실행 가능한 상태CPU 스케줄러가 스레드를 선택하면 실행됨
RUNNING실제로 CPU에서 실행 중인 상태run() 내부 코드가 실행되는 순간
WAITING / TIMED_WAITING다른 스레드 신호를 기다리는 상태 / 일정 시간 동안 대기하는 상태wait(), join(), sleep()
BLOCKED동기화된 객체 잠금(lock) 때문에 실행하지 못하고 대기 중인 상태synchronized 메서드/블록 접근 대기
TERMINATEDrun()이 끝나서 종료된 상태스레드 작업 완료 후

  1. 스레드를 생성하고 start() 를 호출 ➡️ 실행대기열에 저장되어 자신의 차례가 될 때까지 기다림
  • ❗️ 실행대기열은 큐(Queue)와 같은 구조
  1. 실행대기상태에 있다가 자신의 차례가 되면 실행상태가 됨 (실행대기열에 먼저 들어온 스레드가 실행)

  2. 주어진 실행시간이 다 되거나 yield() 를 만나면 다시 실행대기상태가 되고 다음 차례의 스레드가 실행 상태가 됨

  3. 실행 중에 suspend(), sleep(), wait(), join(), I/O block 에 의해 일시정지상태가 될 수 있음.

  • 사용자의 입력을 기다리는 경우를 예
    : 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기상태가 됨
  • ❗️ I/O block: 입출력작업에서 발생하는 지연상태
  1. 지정된 일시정지시간이 다되거나(time-out), notify(), resume(), interrupt() 가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 됨.

  2. 실행을 모두 마치거나 stop() 이 호출되면 스레드는 소멸된다.

  • ❗️ 꼭 번호의 순서대로 스레드가 수행되는 것은 아님

✅ Java는 main 스레드 시작


🧩 스레드 VS 프로세스

구분프로세스(Process)스레드(Thread)
정의실행 중인 프로그램 단위프로세스 안에서 실행되는 작은 실행 단위
메모리각자 독립된 메모리 공간(코드, 데이터, 스택, 힙)같은 프로세스의 메모리 공유(힙, 데이터)
생성 비용상대적으로 무거움상대적으로 가벼움
통신 방법IPC(파이프, 소켓, 메시지 큐 등) 필요변수, 객체 공유 가능 → 동기화 필요
종료 영향하나 종료해도 다른 프로세스 영향 없음프로세스가 끝나면 스레드도 함께 종료됨
병렬 처리독립적프로세스 안에서 병렬 처리 가능
예시Chrome 브라우저 하나, Word 문서Chrome 브라우저의 탭, Word의 자동 저장 스레드
  • 스레드는 프로세스처럼 스케쥴링의 대상이므로, 이 과정에서 컨텍스트 스위칭이 발생한다.
  • 하지만 스레드는 공유하고 있는 메모리 영역 덕분에 컨텍스트 스위칭 때문에 발생하는 오버헤드가 프로세스에 비해 작다.

🧠 스케쥴링

✅ 멀티프로세스 운영체제에서 CPU가 어떤 프로세스(또는 스레드)를 실행할지 결정하는 작업

  • CPU는 한 번에 한 가지 일만 하니까, 여러 프로그램을 동시에 실행하려면 누가 언제 CPU를 쓸지 정해야 한다.
  • 그걸 스케줄러(Scheduler) 가 관리해준다!

⚙️ 컨텍스트 스위칭

✅ 스케줄링에 따라 CPU가 실행 중인 프로세스를 바꿀 때, 현재 상태를 저장하고 다음 프로세스의 상태를 불러오는 과정.

  • 즉, CPU가 “지금 하던 일 멈추고 다른 일 해!” 할 때,
    그 “지금 하던 일”의 정보를 저장해놔야
    나중에 다시 돌아와서 이어서 할 수 있겠지?

  • 스레드의 컨텍스트 정보는 프로세스보다 적기 때문에
    스레드의 컨텍스트 스위칭은 가볍게 행해지는 것이 보통이다.
    하지만, 실제로 스레드와 프로세스의 관계는 JVM구현에 크게 의존한다.

🔹 컨텍스트(Context)란?

  • 프로세스나 스레드의 실행 상태 정보

예를 들면 👇

  • CPU 레지스터 값 (현재 어디까지 실행했는지)
  • 프로그램 카운터(PC)
  • 스택 포인터, 메모리 상태 등

💡 스레드의 생성과 실행

✔️ Thread 클래스를 상속받는 방법
✔️ Runnable 인터페이스를 구현하는 방법

두 방법 모두 스레드를 통해 작업하고 싶은 내용을 run() 메서드에 작성하면 된다.

✔️ start()
: 스레드 실행 할 준비를 해주고, run() 를 실행 해 준다.
: 흐름이 하나 더 생긴다.


📎 Thread 클래스를 상속받는 방법

class Xxx extends Thread {
	pubilc void run() {
    	// 동시에 실행될 코드 작성
    }
}
Xxx x = new Xxx();
x.start();

✏️ 단일 스레드(main Thread) 실행 예시

public class MyThreadExam {
    public static void main(String[] args) {
        String name = Thread.currentThread().getName(); // 현재 스레드가 갖고있는 이름값
        System.out.println("thread name: " + name);
        System.out.println("start!");
        // 1초마다 *를 10번 출력하는 프로그램을 작성하시오.
        for(int i = 0; i < 10; i++) {
            System.out.print("*");
            try {
                Thread.sleep(1000); // 1초마다 쉰다.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 1초마다 +를 10번 출력하는 프로그램을 작성하시오.
        for(int i = 0; i < 10; i++) {
            System.out.print("+");
            try {
                Thread.sleep(1000); // 1초마다 쉰다.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("end!");
    }
}
thread name: main
start!
**********++++++++++end!

동시에 진행하지 않고 *가 끝나야 +가 나온다.

✏️ 멀티 스레드 실행 예시

✔️ MyThread

// 1. Thread 클래스를 상속받는다.
public class MyThread extends Thread {
    private String str;

    public MyThread(String str) {
        this.str = str;
    }

    // 2. run() 메서드를 오버라이딩 한다.
    // 동시에 실행시키고 싶은 코드를 작성한다.
    @Override
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.print(str);
            try {
                Thread.sleep(1000); // 1초마다 쉰다.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

✔️ MyThreadExam

public class MyThreadExam {
    public static void main(String[] args) {
        String name = Thread.currentThread().getName(); // 현재 스레드가 갖고있는 이름값
        System.out.println("thread name: " + name);
        System.out.println("start!");

        MyThread mt1 = new MyThread("*");
        MyThread mt2 = new MyThread("+");

        // 3. Thread는 Start() 메서드를 실행한다.
        mt1.start(); // 새로운 흐름이 생긴다.
        mt2.start();

        System.out.println("end!");
    }
}
thread name: main
start!
*+end!
*+*+*+*+*+*+*++*+*

❗️ 스레드를 배우기 전에는 main() 메서드가 끝나면 종료되었지만 스레드를 배운 이후에는 모든 스레드가 종료되었을 때 프로그램이 종료된다는 것을 알아둬야 한다.


📎 Runnable 인터페이스를 구현하는 방법

Runnable 은 스레드가 아닌 인터페이스이기 때문에 Start() 메서드가 없다.

그래서 Thread가 Runnable을 가지도록 만들어야 한다.

class xxx implements Runnable {
	public void run() {
    	// 동시에 실행 될 코드 작성
    }
}
Xxx x = new Xxx();
Thread t = new Thread(x);
t.start;

✏️ MyRunnable


// 1. Runnable 인터페이스를 구현한다.
public class MyRunnable implements Runnable {
    private String str;

    public MyRunnable(String str) {
        this.str = str;
    }

    // 2. run() 메서드를 오버라이딩 한다.
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("---" + name);

        for(int i = 0; i < 10; i++) {
            System.out.print(str);
            try {
                Thread.sleep(1000); // 1초마다 쉰다.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

✏️ MyRunnableExam

public class MyRunnableExam {
    public static void main(String[] args) {
        String name = Thread.currentThread().getName(); // 현재 스레드가 갖고있는 이름값
        System.out.println("thread name: " + name);
        System.out.println("start!");

        MyRunnable mr1 = new MyRunnable("*");
        MyRunnable mr2 = new MyRunnable("+");

        // 3. Thread 인스턴스를 생성하는데, 생성자에 Runnable 인스턴스를 넣어준다.
        Thread th1  = new Thread(mr1);
        Thread th2 = new Thread(mr2);

        // 4. Thread가 가지고 있는 start() 메서드를 호출한다.
        th1.start();
        th2.start();

        System.out.println("end!");
    }
}

❗️ 실행하다보면 스레드는 서로가 자원을 획득해서 서로가 빨리 실행하고 싶어한다. 그러다보니 실행되는 결과가 항상 똑같지가 않다.

profile
Fill in my own colorful colors🎨

0개의 댓글