Java의 스레드(Thread)
스레드(Thread)는 Java 프로그램에서 병렬 처리를 수행하기 위한 경량 프로세스이다.
Java는 멀티 스레딩을 지원하여 동시에 여러 작업을 수행할 수 있게 한다.
이는 CPU 활용도를 높이고, 응답성을 향상시키는데 유용하다.
일반적인 자바 애플리케이션은 하나의 메인 스레드로 시작하지만, 여러 작업을 동시에 처리하기 위해서는 추가적인 스레드가 필요하다.
특징
- 경량 프로세스
스레드 프로세스 내에서 실행되며, 동일한 메모리 공간을 공유한다.
따라서 스레드 간 통신이 효율적이다.
- 동시성
여러 스레드가 동시에 실행되면서 멀티태스킹을 가능하게 한다. - 상호작용
스레드는 동일한 메모리 공간으로 공유하므로, 서로 데이터를 주고받을 수 있다.
- 운영체제 지원
Java 스레드는 운영체제의 스레드 관리 기능을 사용한다.
스레드 사용 방법
Java에서는 스레드를 생성하고 실행할 때,
1. `Thread` 클래스를 상속받는 방법
2. `Runnable` 인터페이스를 구현하여 스레드를 생성하는 방법
이렇게 두가지 주요 방법 있다.
Thread 클래스 상속
MyThread.java
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
}catch (InterruptedException e){
System.out.println("Thread interrupted");
}
}
}
}
`Thread` 클래스를 상속받고 `run` 메서드를 Overriding 한 것이다.
Main.java (+결과)
public class Main {
public static void main(String[] args) {
Thread thread1 = new MyThread();
Thread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
Thread-1: 0
Thread-0: 0
Thread-1: 1
Thread-0: 1
Thread-0: 2
Thread-1: 2
Thread-1: 3
Thread-0: 3
Thread-0: 4
Thread-1: 4
Runnable 인터페이스 구현
MyRunnable.java
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
}catch (InterruptedException e){
System.out.println("Thread interrupted");
}
}
}
}
Main.java (+결과)
public class Main {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyThread(),"myThread1");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(500);
}catch (InterruptedException e){
System.out.println("Thread interrupted");
}
}
},"myThread2");
// ↑ Runnable 인터페이스는 함수형 인터페이스로 익명구현객체를 통해 thread를 생성할 수도 있다
thread1.start();
thread2.start();
}
}
myThread1: 0
myThread2: 0
myThread1: 1
myThread2: 1
myThread1: 2
myThread2: 2
myThread2: 3
myThread1: 3
myThread2: 4
myThread1: 4
Runnable 인터페이스로 구현하는 방법이 좀 더 유연하다.
💡 위의 두 실행 결과에서 알 수 있듯이, 멀티스레드 환경에서 Java는 스레드의 실행 순서를 보장하지 않는다.
JVM의 스레드 스케줄링에 따라 각 스레드의 실행 순서가 달라질 수 있다.
start() vs run()
`start()` : 새로운 스레드를 시작하고, `run()` 메서드 실행.
`run()` : 코드 블록 실행.
위의 스레드 사용 방법 예시에서 `run()`메서드를 재정의하고 `start()` 메서드를 호출하여 스레드를 실행시켰다.
`run()` 메서드를 직접 호출하면 어떻게 될까?
Main.java (+결과)
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread runnableThread = new Thread(new MyRunnable());
myThread.run(); // 새로운 스레드를 생성 X, 현재 스레드에서 run() 메서드 실행
runnableThread.run(); // 새로운 스레드를 생성 X, 현재 스레드에서 run() 메서드 실행
}
}
main: 0
main: 1
main: 2
main: 3
main: 4
main: 0
main: 1
main: 2
main: 3
main: 4
모든 스레드가 main이라는 이름의 스레드로 실행되고 있는 것과, 병렬처리 구현이 안되어있음을 확인할 수 있다.
`run()`은 코드블록만 실행 시킬 뿐, 새로운 스레드를 실행시키는 명령어가 아니기 때문이다.
`start()`를 호출하면, JVM은 스레드를 위한 호출 스택을 새로 만들어주고,
스레드의 각 호출 스택 안에서 `run()`메서드가 호출된다.
그래서, `start()`메서드를 통해 `run()`을 실행시켜야 새로운 스레드를 실행시킬 수 있고, 병렬처리의 이점을 얻을 수 있다.
주요 메서드
메서드 | 설명 |
start() | 스레드를 시작하고 run() 메서드를 호출 |
run() | 스레드가 실행할 코드를 정의 |
sleep(long millis) | 스레드를 지정된 시간 동안 일시 정지 |
joint() | 특정 스레드가 종료될 때까지 대기 |
yield() | 다른 스레드에게 실행 기회를 양보 |
interrupt() | 스레드에 인터럽트 신호를 보냄 |
스레드 상태 (Thread States)
상태 | 설명 |
NEW | 스레가 생성되었지만 start() 메서드가 호출되지 않은 상태. |
RUNNABLE | 스레드가 실행 중이거나, 실행 준비가 된 상태. 즉, JVM에 의해 스케줄링 되어 실행 될 수 있는 상태. |
BLOCKED | 스레드가 모니터 락을 얻기 위해 기다리는 상태. 다른 스레드가 락을 해제할 때까지 대기. |
WAITING | 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 대기. `Object.wait()`, `Thread.join()` 메서드 호출에 의해 이 상태가 됨. |
TIMED_WAITING | 스레드가 다른 스레드의 특정 작업이 완료되기를 일정 시간 동안 대기. `Object.wait(long timeout)`, `Thread.sleep(long millis)` 메서드 호출에 의해 이 상태가 됨. |
TERMINATED | 스레드의 실행이 종료된 상태. |
주의해야 할 점
동기화 문제
여러 스레드가 동시에 자원에 접근할 때, 동기화(synchronization)를 적절히 하지 않으면 데이터 불일치 문제가 발생할 수 있다. synchronized 키워드나 Lock 클래스를 사용하여 동기화를 관리해야 한다.
데드락(Deadlock)
두 개 이상의 스레드가 서로 자원을 점유한 상태에서 다른 스레드가 점유한 자원을 기다리며 무한 대기 상태에 빠지는 문제.
자원 할당 순서와 타임아웃을 설정하여 데드락을 방지한다.
레이스 컨디션(Race Condition)
여러 스레드가 동시에 공유 자원에 접근하여 발생하는 문제로, 의도치 않은 결과가 발생할 수 있다.
동기화와 원자적 연산을 통해 이를 방지한다.
스레드 풀 사용
스레드 생성과 소멸은 비용이 많이 드는 작업이므로, 스레드 풀(Thread Pool)을 사용하여 스레드를 재사용하는 것이 좋다.
Executors 프레임워크를 사용하여 스레드 풀을 관리할 수 있다.
참고
'Language > Java' 카테고리의 다른 글
직렬화(Serialization) (0) | 2024.12.12 |
---|---|
동기화(synchronization)와 락(Lock) (1) | 2024.12.11 |
Error & Exception (1) | 2024.12.09 |
가비지 컬렉션 (GC; Garbage Collection) (0) | 2024.12.08 |
Inheritance와 Composition (클래스 간의 관계) (0) | 2024.12.07 |