스레드(thread)와 스레드 안전(thread-safe) 개념은 멀티스레드 환경에서 꼭 이해해야 할 중요한 개념들이다.
프로세스라고 부르고, 이 프로세스는 최소 하나 이상의 스레드를 가지고 있다.공유 자원(메모리, 파일 등)을 사용할 수 있다.public 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) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start(); // 스레드 1 실행
thread2.start(); // 스레드 2 실행
}
}
이 코드에서는 두 개의 스레드가 동시에 실행돼서 병렬적으로 작업을 수행한다.

프로세스는 실행 중인 프로그램을 의미하며, 각 프로세스는 독립된 메모리 공간을 가지고 실행된다.스레드는 이러한 프로세스 내에서 실행되는 작은 단위로, 한 프로세스 내에는 여러 개의 스레드가 존재할 수 있다.경쟁 상태(race condition)?
경쟁 상태는 여러 프로세스나 스레드가 동시에 공유 자원에 접근할 때, 예상치 못한 결과가 발생하는 상황을 말한다.
프로그래밍에서 특정 자원을 두 개 이상의 프로세스가 동시에 수정하려고 하면 동기화되지 않은 작업이 발생하면서 의도하지 않은 결과가 나올 수 있다.
public class Counter {
private int count = 0;
public void increment() {
count++; // 여기에 여러 스레드가 동시에 접근하면 문제가 생긴다.
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
}
}
counter.increment() 메서드에 여러 스레드가 동시에 접근해서 값을 수정하려고 한다. public class Counter {
private int count = 0;
// synchronized 키워드를 사용해 스레드 안전성 보장
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
여기서 synchronized 키워드는 increment 메서드에 동시에 하나의 스레드만 접근할 수 있도록 보장한다.
이렇게 하면 여러 스레드가 동시에 increment()를 호출해도 문제가 발생하지 않는다.
synchronized는 단순하지만, 복잡한 상황에서는 더 세밀하게 제어할 수 있는 Lock 객체를 사용할 수 있다.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 마지막엔 항상 락을 해제해야 한다.
}
}
public int getCount() {
return count;
}
}
여기서는 Lock을 명시적으로 사용해서, 하나의 스레드가 락을 잡고 작업을 완료할 때까지 다른 스레드는 그 자원에 접근하지 못하게 막는다.
비동기 프로그래밍에서는 작업이 시작된 이후의 결과를 기다리지 않고 다른 작업을 수행할 수 있는 방식으로 동작한다. 비동기 작업은 이벤트 기반(event-driven)으로 이루어지며, 각 이벤트가 발생할 때 해당 작업이 처리된다.
이벤트 기반(Event-driven) 프로그래밍?
- 이벤트 기반 프로그래밍이란, 프로그램의 흐름이 이벤트에 의해 결정되는 방식이다.
- 예를 들어, 사용자가 버튼을 클릭하면 그 클릭 이벤트에 반응하여 특정 동작을 수행하는 구조이다.
- 비동기 프로그래밍에서는 각 작업이 완료될 때 이벤트가 발생하고, 그 이벤트에 반응하는 코드가 실행된다.
- 이를 통해 작업이 병렬적으로 수행되는 것처럼 느껴지며, 시스템의 응답성을 높일 수 있다.
하지만 멀티스레드 환경에서 비동기 작업과 스레드 동작이 결합되면, 여전히 스레드 안전성 문제를 고려해야 한다.
Atomic 클래스자바에서는 AtomicInteger, AtomicBoolean 같은 원자성(atomicity)을 보장하는 클래스를 제공한다.
원자성이란?
작업이 더 이상 나눌 수 없는 최소 단위로 수행되어 중간에 끼어들 수 없는 성질을 의미한다.
즉, 원자적인 연산은 한 번에 실행되어 다른 스레드가 그 사이에 끼어들 수 없다.
Atomic 클래스들은 내부적으로 동기화 처리가 되어 있어, 멀티스레드 환경에서도 안전하게 사용할 수 있다.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 동기화된 방식으로 증가한다.
}
public int getCount() {
return count.get();
}
}
이렇게 하면 동기화 키워드를 사용하지 않고도 스레드 안전성을 보장할 수 있다.
스레드와 스레드 안전성은 멀티스레드 프로그래밍에서 매우 중요한 개념이다. 이를 안전하게 구현하기 위해 동기화(synchronized), Lock 객체, Atomic 클래스와 같은 다양한 방법을 사용할 수 있다. 비동기 프로그래밍은 응답성을 높이기 위한 유용한 기법이지만, 멀티스레드 환경에서 사용될 경우 여전히 스레드 안전성을 고려해야 한다.