스레드(Thread)는 프로그램 내에서 독립적으로 실행될 수 있는 최소 실행 단위입니다. 자바에서 스레드는 병렬로 여러 작업을 처리하거나 동시에 작업을 실행할 수 있도록 지원합니다. 프로세스 내에서 스레드들은 같은 자원을 공유하면서 실행됩니다.
자바에서 스레드를 사용하는 방법은 크게 두 가지가 있습니다.
Thread 클래스를 상속받는 방법Runnable 인터페이스를 구현하는 방법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번 출력되도록 동작합니다.
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번 출력합니다.
스레드를 사용하는 이유는 크게 병렬 처리와 성능 향상 때문입니다. 여러 작업을 동시에 처리하거나 대기 시간이 긴 작업을 병렬로 처리함으로써 시스템 자원을 효율적으로 사용할 수 있습니다.
여러 작업을 동시에 실행하여 시스템 자원을 효율적으로 사용할 수 있습니다. 예를 들어, 파일 다운로드와 사용자 인터페이스(UI) 처리를 동시에 수행할 수 있습니다.
멀티 코어 CPU에서 여러 스레드를 활용하면 각 코어가 병렬로 작업을 처리하여 성능을 극대화할 수 있습니다.
긴 시간이 걸리는 작업(예: 네트워크 통신, 파일 입출력)은 메인 스레드에서 처리하면 블로킹(blocking)이 발생할 수 있습니다. 이를 별도의 스레드에서 처리하면 메인 스레드가 계속해서 다른 작업을 처리할 수 있습니다.
서버 프로그램에서 각 클라이언트 요청을 별도의 스레드에서 처리함으로써 여러 사용자의 요청을 병렬로 처리할 수 있습니다. 서버가 동시에 많은 요청을 처리해야 할 때 유용합니다.
스레드는 실행 중 다양한 상태로 변경됩니다. 자바에서 스레드의 상태는 다음과 같이 구분됩니다.
스레드가 생성되었지만 아직 실행되지 않은 상태입니다. start() 메서드가 호출되기 전 상태입니다.
스레드가 실행 중이거나 실행할 준비가 된 상태입니다. CPU에 의해 할당되면 실행됩니다.
스레드가 동기화 블록에 들어가기 위해 대기 중인 상태입니다. 다른 스레드가 자원을 점유하고 있으면 블로킹 상태가 됩니다.
스레드가 다른 스레드의 작업이 완료될 때까지 대기하고 있는 상태입니다. Object.wait() 메서드에 의해 대기 상태에 들어갑니다.
스레드가 일정 시간 동안 기다리고 있는 상태입니다. Thread.sleep()이나 Object.wait(long)에 의해 이 상태로 들어갑니다.
스레드의 작업이 완료되어 종료된 상태입니다. run() 메서드가 끝나면 스레드는 종료됩니다.
자바에서는 스레드를 제어하기 위한 여러 가지 메서드를 제공합니다.
start()스레드를 실행시키는 메서드입니다. 이 메서드를 호출하면 스레드가 실행되기 시작하며, run() 메서드가 호출됩니다.
Thread thread = new Thread(new MyRunnable());
thread.start(); // 스레드 시작
sleep(long millis)스레드를 일시 정지시켜 주어진 시간 동안 멈추게 합니다. 시간이 지나면 다시 실행됩니다.
Thread.sleep(1000); // 1초 동안 스레드 정지
join()다른 스레드가 종료될 때까지 기다리도록 현재 스레드를 멈춥니다. 즉, 호출된 스레드가 끝날 때까지 현재 스레드는 기다립니다.
thread.join(); // 해당 스레드가 종료될 때까지 현재 스레드 대기
interrupt()스레드를 중단시킵니다. sleep() 상태에 있는 스레드를 깨울 때 사용되며, InterruptedException이 발생합니다.
thread.interrupt(); // 스레드 중단 요청
isAlive()스레드가 실행 중인지 확인합니다. 스레드가 TERMINATED 상태에 있으면 false를 반환합니다.
boolean alive = thread.isAlive(); // 스레드가 아직 실행 중인지 확인
스레드를 사용하여 동시에 여러 작업을 병렬로 처리하는 방법을 예시로 보여줍니다. 두 개의 스레드를 실행하고, 각 스레드가 작업을 완료할 때까지 기다리는 방식입니다.
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.
*/
thread1.start()와 thread2.start()를 통해 두 개의 스레드가 동시에 실행됩니다.join() 메서드는 해당 스레드가 완료될 때까지 메인 스레드가 대기하게 만듭니다. 따라서 두 스레드가 모두 종료된 후 "Both threads finished." 메시지가 출력됩니다.Thread.sleep(1000)으로 각 스레드는 1초 간격으로 실행되며, 출력 순서는 OS 스케줄러에 의해 달라질 수 있습니다. 이 예시에서는 스레드가 동시에 실행되므로 출력 순서가 교차되어 나타날 수 있습니다.멀티스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근하면 데이터 불일치가 발생할 수 있습니다. 이를 해결하기 위해 동기화 (synchronization)가 필요합니다. 자바는 synchronized 키워드를 사용하여 스레드들이 안전하게 자원에 접근할 수 있도록 합니다.
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
*/
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 키워드: 메서드에 synchronized 키워드를 붙이면 동시에 하나의 스레드만 해당 메서드에 접근할 수 있습니다. 이렇게 하면 스레드 간 충돌을 방지하고, 데이터의 무결성을 보장할 수 있습니다.synchronized 블록 사용method-level 동기화는 메서드 전체에 대해 동기화하기 때문에 비효율적일 수 있습니다. 특정 코드 블록만 동기화하고 싶을 때는 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으로 증가합니다.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 키워드: 이 키워드를 사용하면 변수 값이 여러 스레드에 걸쳐 즉시 반영되며, 캐싱을 방지합니다. 즉, 한 스레드에서 값이 변경되면 다른 스레드에서도 그 변경 사항을 즉시 인식할 수 있습니다.sharedData.isRunning() 값을 반복적으로 체크하면서 루프를 제어합니다. volatile을 사용하지 않으면 한 스레드가 변수 값을 변경해도 다른 스레드에서는 이전 값을 캐시하여 일관성이 깨질 수 있습니다.