1. 스레드 (Thread)란?
스레드(Thread)는 프로그램 내에서 독립적으로 실행될 수 있는 최소 실행 단위입니다. 자바에서 스레드는 병렬로 여러 작업을 처리하거나 동시에 작업을 실행할 수 있도록 지원합니다. 프로세스 내에서 스레드들은 같은 자원을 공유하면서 실행됩니다.
1.1 프로세스 VS 스레드
- 프로세스: 운영체제로부터 독립된 자원을 할당받아 실행되는 프로그램의 단위입니다. 각 프로세스는 서로 독립적이며 자원을 공유하지 않습니다.
- 스레드: 한 프로세스 내에서 여러 작업을 병렬로 실행하는 실행 흐름의 단위입니다. 여러 스레드가 한 프로세스 내에서 자원을 공유하며 작업을 나눠서 처리할 수 있습니다.
2. 자바에서 스레드 사용 방법
자바에서 스레드를 사용하는 방법은 크게 두 가지가 있습니다.
Thread
클래스를 상속받는 방법Runnable
인터페이스를 구현하는 방법
2.1 Thread
클래스를 상속받는 방법
Thread
클래스를 상속받아 run()
메서드를 오버라이드하는 방식입니다. 이 방법은 새로운 클래스를 정의하고, 그 안에서 스레드에서 실행될 코드를 작성하는 방식입니다.
예시 코드
class MyThread extends Thread { // Thread 클래스를 상속받음
public void run() { // run() 메서드를 오버라이드하여 스레드에서 실행할 코드를 정의
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i); // 스레드가 실행되는 동안 5번 반복
try {
Thread.sleep(1000); // 1초간 스레드를 멈춤 (1000밀리초)
} catch (InterruptedException e) {
e.printStackTrace(); // 예외 처리
}
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread(); // 스레드 객체 생성
thread.start(); // 스레드 시작 (run() 메서드 호출)
}
}
// 출력 결과
/*
Thread running: 0
Thread running: 1
Thread running: 2
Thread running: 3
Thread running: 4
*/
thread.start()
를 호출하면, 새로운 스레드가 생성되어run()
메서드가 실행됩니다.Thread.sleep(1000)
을 통해 스레드는 1초씩 대기합니다. 이 코드는 5초 동안 스레드가 5번 출력되도록 동작합니다.
2.2 Runnable
인터페이스를 구현하는 방법
Runnable
인터페이스를 구현하여 run()
메서드 안에 스레드에서 실행될 코드를 작성합니다. 이 방법은 다중 상속이 불가능한 자바의 특성상, 다른 클래스를 상속받는 경우에 유용합니다.
예시 코드
class MyRunnable implements Runnable { // Runnable 인터페이스를 구현
public void run() { // run() 메서드에서 스레드가 실행할 코드를 정의
for (int i = 0; i < 5; i++) {
System.out.println("Runnable running: " + i);
try {
Thread.sleep(1000); // 1초간 멈춤
} catch (InterruptedException e) {
e.printStackTrace(); // 예외 처리
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable()); // Runnable을 스레드로 감쌈
thread.start(); // 스레드 시작 (run() 메서드 실행)
}
}
// 출력 결과
/*
Runnable running: 0
Runnable running: 1
Runnable running: 2
Runnable running: 3
Runnable running: 4
*/
- _
new MyRunnable()
_은Runnable
인터페이스를 구현한 클래스를 생성하고, 이를Thread
객체로 감싼 후start()
메서드를 호출하여 실행합니다. *_Thread.sleep(1000)
_으로 스레드를 1초씩 멈추면서 5번 출력합니다.
3. 왜 스레드를 사용하는가?
스레드를 사용하는 이유는 크게 병렬 처리와 성능 향상 때문입니다. 여러 작업을 동시에 처리하거나 대기 시간이 긴 작업을 병렬로 처리함으로써 시스템 자원을 효율적으로 사용할 수 있습니다.
3.1 스레드를 사용하는 자세한 이유
1) 병렬 처리
- 여러 작업을 동시에 실행하여 시스템 자원을 효율적으로 사용할 수 있습니다.
- 예시: 파일 다운로드와 사용자 인터페이스(UI) 처리를 동시에 수행할 수 있습니다.
2) 성능 향상
- 멀티코어 CPU 환경에서 여러 스레드를 활용해 병렬 처리로 성능을 극대화
3) 비동기 작업 처리
- 네트워크 통신, 파일 입출력과 같은 긴 작업을 별도의 스레드에서 처리해 메인 스레드의 블로킹 방지 (메인 스레드가 계속해서 다른 작업을 처리할 수 있음)
4) 서버와 클라이언트 작업 분리
- 서버에서 각 클라이언트 요청을 별도 스레드로 처리하여 여러 사용자의 요청을 병렬로 처리 및 응답
4. 스레드의 상태
스레드는 실행 중 다양한 상태로 변경됩니다. 자바에서 스레드의 상태는 다음과 같이 구분됩니다.
4.1 스레드의 상태와 전환
1) New
- 스레드가 생성되었지만 아직 실행되지 않은 상태입니다.
start()
메서드가 호출되기 전 상태입니다.
2) Runnable
- 스레드가 실행 중이거나 실행할 준비가 된 상태 (실행 대기 상태)입니다.
- CPU에 의해 할당되면 실행됩니다.
3) Blocked
- 스레드가 동기화 블록에 들어가기 위해 대기 중인 상태입니다.
- 다른 스레드가 자원을 점유하고 있으면 블로킹 상태가 됩니다.
4) Waiting
- 스레드가 다른 스레드의 작업이 완료될 때까지 대기하고 있는 상태입니다.
Object.wait()
메서드에 의해 대기 상태에 들어갑니다.
5) Timed Waiting
- 스레드가 일정 시간 동안 기다리고 있는 상태입니다.
Thread.sleep()
이나Object.wait(long)
에 의해 이 상태로 들어갑니다.
6) Terminated
- 스레드의 작업이 완료되어 종료된 상태입니다.
run()
메서드가 끝나면 스레드는 종료됩니다.
5. 스레드 제어 메서드
자바에서는 스레드를 제어하기 위한 여러 가지 메서드를 제공합니다.
5.1 start()
스레드를 실행시키는 메서드입니다. 이 메서드를 호출하면 스레드가 실행되기 시작하며, run()
메서드가 호출됩니다.
Thread thread = new Thread(new MyRunnable());
thread.start(); // 스레드 시작
5.2 sleep(long millis)
스레드를 일시 정지시켜 주어진 시간 동안 멈추게 합니다. 시간이 지나면 다시 실행됩니다.
Thread.sleep(1000); // 1초 동안 스레드 정지
5.3 join()
다른 스레드가 종료될 때까지 기다리도록 현재 스레드를 멈춥니다. 즉, 호출된 스레드가 끝날 때까지 현재 스레드는 기다립니다.
thread.join(); // 해당 스레드가 종료될 때까지 현재 스레드 대기
5.4 interrupt()
스레드를 중단시킵니다. sleep()
상태에 있는 스레드를 깨울 때 사용되며, InterruptedException
이 발생합니다.
thread.interrupt(); // 스레드 중단 요청
5.5 isAlive()
스레드가 실행 중인지 확인합니다. 스레드가 TERMINATED
상태에 있으면 false
를 반환합니다.
boolean alive = thread.isAlive(); // 스레드가 아직 실행 중인지 확인
6. 스레드 활용 예시
6.1 여러 스레드 실행 및 제어
스레드를 사용하여 동시에 여러 작업을 병렬로 처리하는 방법을 예시로 보여줍니다. 두 개의 스레드를 실행하고, 각 스레드가 작업을 완료할 때까지 기다리는 방식입니다.
예시 코드
class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
// 스레드에서 실행될 코드: 1초마다 메시지를 출력하는 작업을 3번 반복합니다.
for (int i = 0; i < 3; i++) {
System.out.println(name + " running: " + i);
try {
Thread.sleep(1000); // 1초간 멈춤
} catch (InterruptedException e) {
// 스레드가 중단될 경우 예외 처리
System.out.println(name + " interrupted!");
}
}
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
// 두 개의 스레드를 생성하여 실행
Thread thread1 = new Thread(new MyRunnable("Thread 1"));
Thread thread2 = new Thread(new MyRunnable("Thread 2"));
// 스레드 실행
thread1.start(); // 스레드 1 시작
thread2.start(); // 스레드 2 시작
// 메인 스레드는 thread1과 thread2가 모두 끝날 때까지 대기
thread1.join(); // 스레드 1이 끝날 때까지 기다림
thread2.join(); // 스레드 2가 끝날 때까지 기다림
System.out.println("Both threads finished."); // 모든 스레드가 종료된 후 실행
}
}
// 출력 결과
/*
Thread 1 running: 0
Thread 2 running: 0
Thread 1 running: 1
Thread 2 running: 1
Thread 1 running: 2
Thread 2 running: 2
Both threads finished.
*/
- 두 개의 스레드가 각각 1초 간격으로 "running" 메시지를 3번 출력합니다.
thread1.start()
와thread2.start()
*를 통해 두 개의 스레드가 동시에 실행됩니다.join()
메서드는 해당 스레드가 완료될 때까지 메인 스레드가 대기하게 만듭니다. 따라서 두 스레드가 모두 종료된 후 "Both threads finished." 메시지가 출력됩니다.Thread.sleep(1000)
으로 각 스레드는 1초 간격으로 실행되며, 출력 순서는 OS 스케줄러에 의해 달라질 수 있습니다. 이 예시에서는 스레드가 동시에 실행되므로 출력 순서가 교차되어 나타날 수 있습니다.
6.2 스레드 간 동기화
멀티스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근하면 데이터 불일치가 발생할 수 있습니다. 이를 해결하기 위해 동기화 (synchronization)가 필요합니다.
자바는 synchronized
키워드와 volatile
키워드를 제공하여 스레드 간의 자원 접근을 제어합니다.
1) 동기화 없이 자원 접근 시 문제점
동기화가 적용되지 않으면 여러 스레드가 동시에 자원에 접근하여 작업 결과가 예상과 다르게 나올 수 있습니다.
문제 상황 코드
class Counter {
private int count = 0;
// 동기화 없이 카운터를 증가시키는 메서드
public void increment() {
count++; // 여러 스레드가 동시에 접근하면 문제 발생 가능
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 두 개의 스레드가 동일한 카운터를 증가시킴
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment(); // 스레드가 동시에 접근하면 문제가 발생할 수 있음
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join(); // t1이 끝날 때까지 대기
t2.join(); // t2가 끝날 때까지 대기
// 예상 출력은 2000이지만, 실제로는 그보다 작은 값이 출력될 수 있음
System.out.println("Final count without synchronization: " + counter.getCount());
}
}
// 동기화 없이 실행하면, 출력 결과는 예기치 않게 작을 수 있습니다.
// 출력 예시 (동기화 없을 경우, 실행할 때마다 결과가 달라질 수 있음)
/*
Final count without synchronization: 1850
*/
increment()
메서드는 스레드 간에 동기화되지 않아 동시에 실행될 경우 데이터 경쟁(Race Condition) 문제가 발생합니다.- 예를 들어, 두 스레드가 동일한 시점에
count++
를 실행하면 동시에 동일한 값을 읽고 증가하게 되어 최종 결과가 예상치보다 낮을 수 있습니다.
2) 해결책: synchronized
로 동기화
synchronized
키워드를 사용하여 자원에 대한 접근을 한 번에 하나의 스레드만 허용함으로써 데이터 불일치 문제를 방지할 수 있습니다.
동기화 된 코드
class Counter {
private int count = 0;
// synchronized 키워드를 사용하여 동기화된 카운터 증가
public synchronized void increment() {
count++; // 여러 스레드가 접근하더라도 한 번에 하나의 스레드만 접근 가능
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 두 개의 스레드가 동일한 카운터를 증가시킴
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join(); // t1이 끝날 때까지 대기
t2.join(); // t2가 끝날 때까지 대기
// 동기화 덕분에 항상 2000이 출력됨
System.out.println("Final count with synchronization: " + counter.getCount());
}
}
// 출력 결과 (동기화된 경우):
/*
Final count with synchronization: 2000
*/
synchronized
메서드: 메서드 전체를 동기화하여 여러 스레드가 동시에 호출할 수 없도록 보장합니다.- 한 스레드가
increment()
메서드를 실행하는 동안 다른 스레드는 해당 메서드에 접근할 수 없습니다. - 이를 통해 카운터 값이 정확히 증가하도록 보장합니다.
3) 효율적 동기화: synchronized
블록 사용
메서드 전체를 동기화하면 불필요하게 많은 코드가 동기화 대상이 되어 성능 저하를 유발할 수 있습니다. 따라서 필요한 코드 부분만 synchronized
블록으로 동기화하는 것이 더 효율적입니다.
동기화 블록 코드
class Counter {
private int count = 0;
private int anotherCount = 0; // 두 번째 카운터 변수
// synchronized 블록을 사용하여 필요한 부분만 동기화
public void incrementBoth() {
synchronized (this) {
count++; // 이 블록은 한 번에 하나의 스레드만 접근 가능
anotherCount++; // 두 변수 모두 안전하게 동기화됨
}
}
public int getCount() {
return count;
}
public int getAnotherCount() {
return anotherCount;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementBoth(); // 두 변수를 동시에 증가시킴
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementBoth(); // 두 변수를 동시에 증가시킴
}
});
t1.start();
t2.start();
t1.join(); // t1이 끝날 때까지 대기
t2.join(); // t2가 끝날 때까지 대기
System.out.println("Final count: " + counter.getCount());
System.out.println("Final anotherCount: " + counter.getAnotherCount());
}
}
// 출력 결과:
/*
Final count: 2000
Final anotherCount: 2000
*/
synchronized(this)
블록 내에서는count
와anotherCount
두 변수를 모두 증가시키고 있습니다. 이 블록은 동시에 하나의 스레드만 접근할 수 있으므로, 두 변수 모두 안전하게 동기화되어 정확하게 2000으로 증가합니다.- 만약 이 동기화 블록이 없었다면, 두 스레드가 동시에 두 변수를 증가시키는 작업을 할 수 있기 때문에 결과값이 예측할 수 없게 될 수 있습니다.
- 이 방법은 메서드 전체를 동기화하지 않고 필요한 코드 부분만 동기화하여 효율성을 높입니다.
4) 경량 동기화: volatile
키워드 사용
동기화 대신 volatile
키워드를 사용하여 변수의 일관성을 보장할 수 있습니다. volatile
은 변수 값을 스레드 간에 공유하고, 캐싱 문제를 방지하지만, 동기화 블록처럼 동시 실행 제어는 제공하지 않습니다.
class SharedData {
private volatile boolean running = true; // volatile로 변수 선언
public void stop() {
running = false; // 다른 스레드에서 값을 바꾸면 모든 스레드에 즉시 반영됨
}
public boolean isRunning() {
return running;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
SharedData sharedData = new SharedData();
Thread worker = new Thread(() -> {
while (sharedData.isRunning()) { // running이 true일 동안 루프
System.out.println("Worker is running");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Worker stopped");
});
worker.start(); // 스레드 시작
Thread.sleep(2000); // 2초 대기 후
sharedData.stop(); // worker 스레드를 중지시킴
worker.join(); // worker 스레드가 끝날 때까지 기다림
}
}
// 출력 결과:
/*
Worker is running
Worker is running
Worker is running
Worker is running
Worker stopped
*/
volatile
로 선언된 변수는 항상 메인 메모리에서 읽고 쓰기 때문에, 한 스레드가 값을 변경하면 다른 스레드에서 즉시 반영됩니다. (여러 스레드에 걸쳐 즉시 반영되며, 캐싱을 방지)- 하지만,
volatile
은 변수의 일관성만 보장하며,synchronized
처럼 자원의 동시 접근을 제어하지는 못합니다. - 이 코드는
sharedData.isRunning()
값을 반복적으로 체크하면서 루프를 제어합니다.volatile
을 사용하지 않으면 한 스레드가 변수 값을 변경해도 다른 스레드에서는 이전 값을 캐시하여 일관성이 깨질 수 있습니다.
5) 동기화 방법 비교
동기화 방식 | 설명 | 적용 사례 |
---|---|---|
synchronized 메서드 |
메서드 전체를 동기화 | 여러 스레드가 자원을 공유하며 복잡한 연산을 수행할 때 |
synchronized 블록 |
특정 코드 블록만 동기화 | 메서드 전체가 아닌 일부 코드만 동기화가 필요할 때 |
volatile 키워드 |
변수의 일관성만 보장 (경량) | 단순 플래그 변수나 값 변경을 즉시 반영해야 할 때, 복잡한 연산(예: 증가 연산) 대신 간단한 읽기/쓰기 작업에 사용 |
7. 정리
- 스레드(Thread): 프로그램 내에서 독립적으로 실행될 수 있는 최소 실행 단위로, 병렬 처리와 성능 최적화를 위해 사용됩니다.
- 생성 방법:
Thread
클래스 상속 또는Runnable
인터페이스 구현 - 스레드 상태: New, Runnable, Blocked, Waiting, Timed Waiting, Terminated로 구분
- 스레드 동기화:
synchronized
키워드로 자원 충돌 방지,volatile
로 변수 일관성 보장 - 스레드 제어 메서드:
start()
,sleep()
,join()
,interrupt()
,isAlive()
등이 주요 메서드 - 멀티스레드 주의사항: 데이터 불일치를 방지하기 위해 동기화 사용 및 병렬 처리 효율성 고려
'BackEnd > JAVA' 카테고리의 다른 글
[JAVA] 와일드카드 (Generic Wildcards) 정리 (1) | 2025.02.05 |
---|---|
[JAVA] 제네릭 (Generic) 정리 (1) | 2025.01.26 |
[JAVA] 인터페이스 심화: 다중 상속, 다중 구현 (3) | 2025.01.20 |
[JAVA] try-with-resources 정리 (0) | 2025.01.19 |
[JAVA] 자바 부모클래스 및 인터페이스 심화: 오버라이딩과 메서드 동작 (0) | 2025.01.17 |