스레드는 프로세스 내에서 독립적으로 실행되는 하위 작업입니다.
멀티스레딩 기술 사용 시나리오
- 블로킹(Blocking): 시스템에 블로킹 현상이 발생하면 멀티스레딩 기술을 사용하여 실행 효율을 높일 수 있습니다.
- 종속성(Dependency): 비즈니스 로직이 두 개의 실행 과정 A와 B로 나뉘는데, A 과정이 블로킹될 때 B 과정은 A 과정의 실행 결과를 필요로 하지 않는다면, B 과정에 멀티스레딩 기술을 적용하여 실행 효율을 향상시킬 수 있습니다.
멀티스레딩은 비동기적으로 동작합니다. CPU는 시간 할당(Time Slice)을 여러 스레드에 분배하며, 스레드는 할당된 시간 동안 작업을 수행합니다. CPU가 스레드 간 전환 시 오버헤드가 발생하므로, 스레드 수가 많다고 반드시 성능이 향상되는 것은 아니며, 오히려 과도한 스레드 생성은 성능 저하를 유발할 수 있습니다.
멀티스레딩 구현 방법
Java에서 멀티스레딩을 구현하는 두 가지 주요 방법이 있습니다:
Thread클래스 상속Runnable인터페이스 구현
Thread 클래스는 이미 Runnable 인터페이스를 구현하고 있습니다. Thread 클래스를 상속하여 스레드를 생성하는 경우, Java의 다중 상속 제약으로 인해 다른 클래스를 상속받는 데 제약이 따릅니다.
1. Thread 클래스 상속을 통한 스레드 생성
MyThread.java
package thread;
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread 실행 중");
}
}
MyThreadRun.java
package thread;
public class MyThreadRun {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 스레드 시작 (시간 소요)
System.out.println("메인 스레드 종료"); // 메인 스레드 작업 (시간 적게 소요)
}
}
2. Runnable 인터페이스 구현을 통한 스레드 생성
MyRunnable.java
package thread;
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable 실행 중");
}
}
MyRunnableRun.java
package thread;
public class MyRunnableRun {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println("메인 스레드 종료");
}
}
데이터 공유: 동시성 제어
여러 스레드가 동일한 변수에 접근하여 데이터를 공유할 수 있습니다. 예를 들어, 티켓 판매 시스템에서는 여러 스레드가 동시에 티켓 수를 감소시킬 수 있습니다.
synchronized 키워드를 사용하여 여러 스레드가 run() 메소드를 실행할 때 순차적으로 처리되도록 동기화를 적용할 수 있습니다. synchronized는 객체 또는 메소드에 잠금을 설정하며, 잠금이 적용된 코드를 "상호 배제 구역(Critical Section)"이라고 합니다.
synchronized는 메서드 호출 시 `ACC_SYNCHRONIZED` 플래그를 확인하여 동기화를 수행합니다. 플래그가 설정되어 있으면, 해당 스레드는 동기화 잠금을 획득하고 메서드를 실행한 후, 메서드 완료 시 잠금을 해제합니다.
데이터 공유 예제 (synchronized 사용)
MyThreadSharedData.java
package thread;
public class MyThreadSharedData extends Thread {
private int ticketCount = 5; // 총 티켓 수
@Override
// synchronized 키워드를 사용하여 해당 메소드를 동기화
public synchronized void run() {
super.run();
if (ticketCount > 0) {
ticketCount--;
System.out.println(Thread.currentThread().getName() + " 티켓 판매. 남은 티켓: " + ticketCount);
}
}
}
MyThreadSharedDataRun.java
package thread;
public class MyThreadSharedDataRun {
public static void main(String[] args) {
MyThreadSharedData sellTicketThread = new MyThreadSharedData();
// 여러 스레드 생성 및 시작
Thread thread1 = new Thread(sellTicketThread, "Thread-1");
Thread thread2 = new Thread(sellTicketThread, "Thread-2");
Thread thread3 = new Thread(sellTicketThread, "Thread-3");
Thread thread4 = new Thread(sellTicketThread, "Thread-4");
Thread thread5 = new Thread(sellTicketThread, "Thread-5");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
실행 결과 (예상)
출력 순서는 스레드 스케줄링에 따라 달라질 수 있으며, start() 호출 순서가 run() 실행 순서를 보장하지 않습니다.
비동기 문제(Non-thread-safe): 여러 스레드가 동일한 객체의 인스턴스 변수에 동시에 접근하여 수정할 때, 값의 불일치나 예기치 않은 변경이 발생하여 프로그램 흐름에 영향을 줄 수 있습니다.
run(): 현재 스레드에서 즉시 run() 메소드를 실행합니다. 새로운 스레드를 생성하지 않습니다.
start(): 새로운 스레드를 생성하고, 해당 스레드에서 run() 메소드를 실행합니다. 실행 시점은 스케줄링에 따라 달라집니다.
스레드 중지: 예외 처리 활용
스레드를 중지시키는 한 가지 방법은 스레드 내에서 `InterruptedException`을 발생시키는 것입니다.
MyInterruptThread.java
package thread;
public class MyInterruptThread extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 500000; i++) {
// 스레드가 중단 요청을 받았는지 확인
if (this.isInterrupted()) {
System.out.println("중단 상태 감지, 스레드 종료.");
throw new InterruptedException(); // InterruptedException 발생시켜 catch 블록으로 이동
}
System.out.println("i = " + i);
}
System.out.println("for 루프 종료 후"); // 정상적으로 루프를 마쳤을 경우
} catch (InterruptedException e) {
System.out.println("MyInterruptThread.run() 메소드의 catch 블록 진입");
e.printStackTrace(); // 예외 스택 트레이스 출력
}
}
}
MyInterruptThreadRun.java
package thread;
public class MyInterruptThreadRun {
public static void main(String[] args) {
try {
MyInterruptThread thread = new MyInterruptThread();
thread.start();
Thread.sleep(2000); // 메인 스레드를 2초간 일시 중지
thread.interrupt(); // 작업 스레드에 중단 요청
} catch (InterruptedException e) {
System.out.println("main 메소드의 catch 블록 진입");
e.printStackTrace();
}
System.out.println("메인 스레드 종료");
}
}
스레드 종류: 일반 스레드와 데몬 스레드
Java에는 두 종류의 스레드가 있습니다: 일반 스레드(사용자 스레드)와 데몬 스레드(Daemon Thread).
데몬 스레드는 일반 스레드가 모두 종료되면 자동으로 소멸되는 보조적인 스레드입니다. 대표적인 예로 가비지 컬렉션 스레드가 있습니다.
synchronized 키워드 상세
SynchronizedMethodTest.java
package thread;
public class SynchronizedMethodTest {
// static 메소드에 synchronized 키워드 적용
synchronized public static void testMethod() {
System.out.println("testMethod() 호출됨");
}
public static void main(String[] args) throws InterruptedException {
testMethod();
}
}
javap -c -v SynchronizedMethodTest.class 명령어를 사용하여 해당 클래스의 바이트코드를 분석하면, `ACC_SYNCHRONIZED` 플래그가 메소드에 적용된 것을 확인할 수 있습니다.
동기화 블록 (synchronized block)
동기화 블록을 사용하면 특정 코드 영역에 대해 동기화를 적용할 수 있습니다. 이 경우, JVM은 `monitorenter`와 `monitorexit` 명령어를 사용하여 동기화를 처리합니다.
SynchronizedBlockTest.java
package thread;
public class SynchronizedBlockTest {
public void testMethod() {
// 'this' 객체를 잠금 대상으로 하는 동기화 블록
synchronized (this) {
int age = 100; // 예시 변수
System.out.println("testMethod() 내부 동기화 블록 실행");
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlockTest test = new SynchronizedBlockTest();
test.testMethod();
}
}
javap -c -v SynchronizedBlockTest.class 명령어를 사용하여 바이트코드를 분석하면, `monitorenter`와 `monitorexit` 명령어가 사용된 것을 확인할 수 있습니다.
주의사항: 스레드에서 예외가 발생하면 해당 스레드가 보유하고 있던 모든 잠금이 해제됩니다. 단, `Thread.suspend()` 및 `Thread.sleep(mills)` 메소드는 잠금을 해제하지 않습니다.
volatile 키워드
volatile 키워드는 다음 세 가지 특성을 보장합니다:
- 가시성(Visibility): 한 스레드에서 변경한 값이 다른 스레드에게 즉시 반영됩니다.
- 원자성(Atomicity): (주의:
volatile자체만으로는 복잡한 연산의 원자성을 보장하지 않습니다. 단일 변수 읽기/쓰기에 대한 원자성을 제공합니다.) - 재정렬 방지(No reordering): 컴파일러나 CPU가 명령어 실행 순서를 임의로 재정렬하는 것을 방지합니다.
synchronized 키워드와 volatile 키워드 비교
synchronized 키워드의 특징:
- 가시성:
synchronized는 잠금 획득 시점에 이전 스레드가 변경한 값을 읽어오고, 잠금 해제 시점에 변경된 값을 저장하므로 가시성을 제공합니다. - 원자성: 동기화된 코드는 한 번에 하나의 스레드만 실행할 수 있으므로 원자성을 보장합니다.
- 재정렬 방지: 동기화 블록 내의 코드에 대한 재정렬을 방지합니다.
synchronized와 volatile 사용 시나리오 요약
volatile: 공유 변수의 값이 변경되었을 때 다른 스레드들이 항상 최신 값을 읽도록 보장해야 할 때 사용합니다.synchronized: 여러 스레드가 동일한 객체의 인스턴스 변수에 접근하여 수정할 때, 데이터의 일관성을 유지하고 스레드 안전성 문제를 방지하기 위해 사용합니다.