❗️ Java 멀티 쓰레드 프로그래밍을 익히고 Server에 어떻게 사용되는지 이해해보자
❗️ 순차적 실행이 병렬실행 보다 빠른 경우도 있다. 동시 실행에 따르는 오버헤드가 없고, 단일 CPU 알고리즘은 하드웨어 작업에 더 친화적일 수 있기 때문이다.
✔️ 단순히 실행중인 프로그램
각각의 프로세스들은 자신만의 메모리 영역을 확보한 상태로 실행되고 있다는 것을 알 수 있다.
✔️ 프로세스 간의 통신하는 방법
종류 | 설명 | Java에서 쓰는 방법 |
---|---|---|
파이프(Pipe) | 한 프로세스 출력 → 다른 프로세스 입력 | PipedInputStream , PipedOutputStream |
메시지 큐(Message Queue) | 메시지를 큐에 넣고 읽음 | JMS, RabbitMQ 등 |
공유 메모리(Shared Memory) | OS가 특정 메모리를 여러 프로세스가 공유 | Java는 직접 불가, JNI 통해 가능 |
소켓(Socket) | 네트워크 기반 IPC | Socket , ServerSocket |
신호(Signal) | 이벤트 발생 알림 | OS 신호 (Java는 제한적) |
✔️ 실행 단위
✏️ 예시 비유:
👉 직원이 각자 일을 처리하면서 동시에 돌아감
❗️ 특정시간에 Thread1 특정시간에 Thread2 이렇게 시간을 쪼개서 각각의 스레드가 실행됐다 안됐다를 반복하면서 실행하게 된다.
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();
}
}
Java에서 스레드는 생명주기(lifecycle)를 가지고 있고, 각 상태를 이해하면 디버깅이나 설계할 때 도움이 된다.
상태(State) | 설명 | 예시 |
---|---|---|
NEW | 스레드 객체가 생성되었지만, 아직 start() 안 한 상태 | Thread t = new Thread(); |
RUNNABLE | start() 가 호출되어 실행 대기 중 또는 실행 가능한 상태 | CPU 스케줄러가 스레드를 선택하면 실행됨 |
RUNNING | 실제로 CPU에서 실행 중인 상태 | run() 내부 코드가 실행되는 순간 |
WAITING / TIMED_WAITING | 다른 스레드 신호를 기다리는 상태 / 일정 시간 동안 대기하는 상태 | wait() , join() , sleep() 등 |
BLOCKED | 동기화된 객체 잠금(lock) 때문에 실행하지 못하고 대기 중인 상태 | synchronized 메서드/블록 접근 대기 |
TERMINATED | run() 이 끝나서 종료된 상태 | 스레드 작업 완료 후 |
start()
를 호출 ➡️ 실행대기열에 저장되어 자신의 차례가 될 때까지 기다림실행대기상태에 있다가 자신의 차례가 되면 실행상태가 됨 (실행대기열에 먼저 들어온 스레드가 실행)
주어진 실행시간이 다 되거나 yield()
를 만나면 다시 실행대기상태가 되고 다음 차례의 스레드가 실행 상태가 됨
실행 중에 suspend()
, sleep()
, wait()
, join()
, I/O block
에 의해 일시정지상태가 될 수 있음.
지정된 일시정지시간이 다되거나(time-out
), notify()
, resume()
, interrupt()
가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 됨.
실행을 모두 마치거나 stop()
이 호출되면 스레드는 소멸된다.
구분 | 프로세스(Process) | 스레드(Thread) |
---|---|---|
정의 | 실행 중인 프로그램 단위 | 프로세스 안에서 실행되는 작은 실행 단위 |
메모리 | 각자 독립된 메모리 공간(코드, 데이터, 스택, 힙) | 같은 프로세스의 메모리 공유(힙, 데이터) |
생성 비용 | 상대적으로 무거움 | 상대적으로 가벼움 |
통신 방법 | IPC(파이프, 소켓, 메시지 큐 등) 필요 | 변수, 객체 공유 가능 → 동기화 필요 |
종료 영향 | 하나 종료해도 다른 프로세스 영향 없음 | 프로세스가 끝나면 스레드도 함께 종료됨 |
병렬 처리 | 독립적 | 프로세스 안에서 병렬 처리 가능 |
예시 | Chrome 브라우저 하나, Word 문서 | Chrome 브라우저의 탭, Word의 자동 저장 스레드 |
✅ 멀티프로세스 운영체제에서 CPU가 어떤 프로세스(또는 스레드)를 실행할지 결정하는 작업
✅ 스케줄링에 따라 CPU가 실행 중인 프로세스를 바꿀 때, 현재 상태를 저장하고 다음 프로세스의 상태를 불러오는 과정.
즉, CPU가 “지금 하던 일 멈추고 다른 일 해!” 할 때,
그 “지금 하던 일”의 정보를 저장해놔야
나중에 다시 돌아와서 이어서 할 수 있겠지?
스레드의 컨텍스트 정보는 프로세스보다 적기 때문에
스레드의 컨텍스트 스위칭은 가볍게 행해지는 것이 보통이다.
하지만, 실제로 스레드와 프로세스의 관계는 JVM구현에 크게 의존한다.
예를 들면 👇
✔️ Thread 클래스를 상속받는 방법
✔️ Runnable 인터페이스를 구현하는 방법
두 방법 모두 스레드를 통해 작업하고 싶은 내용을 run()
메서드에 작성하면 된다.
✔️ start()
: 스레드 실행 할 준비를 해주고,run()
를 실행 해 준다.
: 흐름이 하나 더 생긴다.
class Xxx extends Thread {
pubilc void run() {
// 동시에 실행될 코드 작성
}
}
Xxx x = new Xxx();
x.start();
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
은 스레드가 아닌 인터페이스이기 때문에 Start()
메서드가 없다.
그래서 Thread가 Runnable을 가지도록 만들어야 한다.
class xxx implements Runnable {
public void run() {
// 동시에 실행 될 코드 작성
}
}
Xxx x = new Xxx();
Thread t = new Thread(x);
t.start;
// 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();
}
}
}
}
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!");
}
}
❗️ 실행하다보면 스레드는 서로가 자원을 획득해서 서로가 빨리 실행하고 싶어한다. 그러다보니 실행되는 결과가 항상 똑같지가 않다.