0. 들어가기 전
멀티스레드를 사용할 때 가장 주의해야 할 점은, 같은 자원(리소스)에 여러 스레드가 동시에 접근할 때 발생하는 동시성 문제 (concurrency issue)입니다. 이처럼 여러 스레드가 동시에 접근하는 자원을 공유 자원(shared resource)이라고 하며, 대표적으로는 클래스의 인스턴스 필드(멤버 변수)가 이에 해당합니다. 멀티스레드 환경에서는 이러한 공유 자원에 대한 접근을 적절히 동기화(synchronization) 하지 않으면, 예기치 않은 결과나 데이터 불일치 문제가 발생할 수 있습니다.
동시성 문제가 실제로 어떤 상황에서 발생하는지 이해하기 위해, 간단한 은행 출금 예제를 통해 살펴보겠습니다.
이 인터페이스는 은행 계좌에서 출금 기능을 제공하며, 다음 두 가지 메서드를 포함합니다.
public interface BankAccount {
boolean withdraw(int amount);
int getBalance();
}
- withdraw(amount): 주어진 금액을 출금한다. (잔액이 충분하면 출금에 성공하고 true, 부족하면 실패하고 false 반환)
- getBalance(): 현재 계좌의 잔액을 반환한다.
public class BankAccountV1 implements BankAccount {
private int balance;
public BankAccountV1(int initialBalance) {
this.balance = initialBalance;
}
@Override
public boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
// 잔고가 출금액 보다 적으면, 진행하면 안됨.
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
// 잔고가 출금액 보다 많으면, 진행.
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금에 걸리는 시간으로 가정(1초)
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public int getBalance() {
return balance;
}
}
- int balance : 계좌의 잔액 필드
public class WithdrawTask implements Runnable {
private BankAccount account;
private int amount;
public WithdrawTask(BankAccount account, int amount) {
this.account = account;
this.amount = amount;
}
@Override
public void run() {
account.withdraw(amount);
}
}
- 출금을 담당하는 Runnable 구현체이다.
- 생성시 출금할 계좌(account)와 출금할 금액(amount)을 저장해 둔다.
public class BankMain {
public static void main(String[] args) throws InterruptedException {
BankAccountV1 account = new BankAccountV1(1000);
Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
t1.start();
t2.start();
sleep(500); // 검증 완료까지 잠시 대기
log("t1 state: " + t1.getState());
log("t2 state: " + t2.getState());
t1.join();
t2.join();
log("최종 잔액: " + account.getBalance());
}
}
- 초기 잔액을 1000 원으로 설정한다.
- main스레드는 t1 , t2 스레드를 만든다.
- 만든 스레드들은 같은 계좌에 각각 800 원의 출금을 시도한다.
- main 스레드는 join() 을 사용해서 t1 , t2 스레드가 출금을 완료한 이후에 최종 잔액을 확인한다.
t1.start( ), t2.start( ) 호출 직후의 메모리 그림
- 각 스레드의 스택에서 run() 실행됨.
- t1은 WithdrawTask(x002)의 run() 호출.
- t2는 WithdrawTask(x003)의 run() 호출.
- 스택 프레임의 this는 호출한 메서드의 인스턴스 참조를 가리킴.
- 두 스레드는 동일한 계좌(x001)에 대해 출금 시도.
- t1의 run()에서 withdraw() 실행.
- 거의 동시에 t2의 run()에서도 withdraw() 실행.
- t1과 t2는 같은 BankAccount(x001) 인스턴스의 withdraw() 호출.
- 두 스레드는 동일한 BankAccount(x001)에 접근하고, 내부 balance 필드를 함께 사용.

이 시나리오는 한 사용자가 두 대의 PC에서 동시에 같은 계좌에서 출금을 시도하는 상황을 가정합니다.
t1과 t2 스레드는 거의 동시에 실행되지만, 아주 짧은 시간 차이로 t1이 먼저 실행되고 그다음에 t2가 실행됩니다.
계좌의 초기 잔액은 1000원입니다. t1 스레드가 800원을 출금하면 잔액은 200원이 됩니다.
이제 t2 스레드는 잔액보다 많은 800원을 출금하려고 시도하므로, 출금은 실패해야 합니다.
하지만 실제 실행 결과는 예상과 다르게 나타납니다. t1과 t2 모두 출금에 성공하여 총 1600원이 출금됩니다.
결과적으로 계좌 잔액은 -600원이 되며, 시스템은 사용자가 보유한 금액보다 더 많은 돈을 출금할 수 있도록 허용한 셈이 됩니다.
분명히 잔액을 확인하는 조건문이 있음에도 이런 문제가 발생하는 이유는,
두 스레드가 거의 동시에 같은 잔액을 읽고 출금 조건을 통과한 다음, 각각 잔액을 차감했기 때문입니다.
이처럼 공유 자원에 대한 동기화가 이루어지지 않으면, 멀티스레드 환경에서 예기치 못한 동시성 문제가 발생할 수 있습니다.
계좌 출금시 잔고 체크 로직
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance); return false;
}
🤔 volatile 을 사용하면 문제가 해결될까?
balance에 volatile을 써도 문제는 해결되지 않습니다.
volatile은 한 스레드에서 변경한 값을 다른 스레드가 즉시 볼 수 있도록 해주는 메모리 가시성 문제만 해결합니다.
출금은 값 확인부터 차감까지 이어지는 연산이기 때문에, 동기화 없이 처리하면 동시성 문제가 발생할 수 있습니다.
즉, volatile은 가시성만 보장할 뿐, 연산 자체를 안전하게 만들지는 못합니다.
👉 volatile 키워드에 대한 자세한 내용이 궁금하다면 이 글을 참고해주세요.
[JAVA] volatile과 메모리 가시성
👀 메모리 가시성동시에 여러 스레드가 동작하는 멀티스레드 환경에서는 하나의 변수를 여러 스레드가 읽고 쓰는 일이 자주 발생한다.그런데 간단해 보이는 코드에서도, 특정 스레드가 변경한
madeprogame.tistory.com
1. 동시성 문제
왜 이런 문제가 발생하는지 하나씩 천천히 분석해봅시다.
t1, t2 스레드가 순서대로 실행된다고 가정하고, t1이 아주 약간 먼저 실행되는 상황부터 보겠습니다.
t1, t2 순서로 실행 가정
- t1이 약간 먼저 실행되면서 출금을 시도합니다.
- t1은 출금 코드에 있는 검증 로직을 실행하고, 잔액이 출금 금액보다 많은지 확인합니다.
- 이때 잔액은 1000원이고 출금 금액은 800원이므로, 조건을 만족해 검증을 통과합니다.
- t1: 출금 검증 로직을 통과한 후, 실제 출금을 위해 잠시 대기 중입니다. 여기서는 출금에 시간이 걸리는 상황을 가정합니다.
- t2: 검증 로직을 실행해 잔액이 출금 금액보다 많은지 확인합니다.
- 잔액이 1000원이고 출금 금액이 800원이므로, t2 역시 검증을 통과합니다.
🚨 바로 이 부분이 문제입니다!
t1이 아직 잔액을 차감하지 않아, t2는 잔액이 1000원인 상태로 판단한 채 출금을 허용하는 것입니다.
t1이 잔액을 바로 줄였다면 문제가 없겠지만, 잔액 차감 전에 t2가 검증을 통과해버린 셈입니다.
sleep(1000) 코드를 제거해도 완벽한 해결책은 아닙니다.
t1이 잔액을 차감하기 직전 문맥 전환(context switch)이 발생하면, t2가 그 틈을 타 검증을 통과할 수 있기 때문입니다.
sleep은 문제를 재현하기 쉽게 하기 위한 임시 코드일 뿐, 문제의 본질은 동기화되지 않은 공유 자원 접근에 있습니다.
결과적으로 t1과 t2가 동시에 검증을 통과하고 출금을 진행하면서, t1이 잔액 1000원에서 800원을 차감해 200원이 된 상태에서,
t2도 잔액 200원에서 800원을 차감하여 잔액이 -600원이 됩니다. 이렇게 동시성 문제로 인해 은행 계좌의 잔액이 실제보다 적은 -600원이 되는 상황이 발생합니다.
이번에는 t1, t2가 완전히 동시에 실행되는 상황을 알아봅시다.
t1, t2 동시에 실행 가정
- t1과 t2는 동시에 검증 로직을 실행하여 잔액이 출금 금액보다 많은지 확인한다.
- 잔액이 1000원이고 출금 금액이 800원이므로 두 스레드 모두 검증을 통과한다.
- t1과 t2가 동시에 실행되어 각각 800원을 출금할 때, 두 스레드가 잔액을 확인하는 시점의 잔액은 모두 1000원입니다.
- 따라서 t1과 t2는 각각 출금 후 잔액을 200원으로 계산하지만, 두 계산 결과가 동시에 반영되어 최종 잔액은 200원이 됩니다.
balance = balance - amount; 코드는 다음 세 단계로 이루어집니다.
- 오른쪽 balance와 amount 값을 읽어온다. (1000원)
- 두 값을 계산한다. (t1: 200원, t2: 200원)
- 계산 결과를 왼쪽 balance 변수에 저장한다. (balance = 200;)

쉽게 설명하자면, t1과 t2 두 스레드가 동시에 x001.balance 값을 읽으면, 둘 다 1000원을 읽게 됩니다.
따라서 두 스레드는 각각 1000 - 800을 계산해 200이라는 결과를 만듭니다.
마지막으로 두 스레드가 각각 balance = 200을 대입하면서 최종 값은 200원이 됩니다.
은행 입장에서 보면 총 1600원이 빠져나갔는데, 잔액은 800원만 줄어들었습니다.
800원이 감쪽같이 어디론가 사라진 것입니다. 이 문제가 왜 발생했고, 또 이런 문제를 어떻게 해결할 수 있을까요?
임계 영역
이런 문제가 발생하는 근본 원인은 여러 스레드가 공유 자원을 여러 단계로 나누어 사용하는 데 있습니다.
두 단계가 분리되어 있고, 동시에 실행될 수 있기 때문에 검증을 통과한 여러 스레드가 동시에 출금에 성공하는 잘못된 상황이 발생합니다.
- 검증 단계: 잔액(balance)이 출금액(amount)보다 많은지 확인
- 출금 단계: 잔액(balance)에서 출금액(amount)만큼 차감
이 로직에는 하나의 중요한 가정이 있습니다.
출금 메서드 내부에서 검증한 잔액(balance)은, 출금 계산이 끝날 때까지 그대로 유지되어야 한다는 점입니다.
예를 들어 잔액이 1000원일 때 800원을 출금하면, 남은 잔액은 200원이 되어야 정확합니다.
즉, 검증한 값을 기반으로 계산까지 이어져야 하는데, 그 사이에 잔액이 변경된다면 잘못된 결과가 나올 수 있습니다.
만약 다른 스레드가 중간에 balance 값을 바꿔버리면, 내가 읽은 1000원이 실제로는 이미 변경된 값일 수 있습니다.
(시나리오는 출금 기준이지만, 입금도 같은 문제가 발생할 수 있습니다)
🚨 공유 자원 문제
잔액(balance)은 여러 스레드가 함께 사용하는 공유 자원입니다.
여기서는 출금() 메서드가 호출될 때만 balance 값이 바뀝니다.
하지만 동시에 여러 스레드가 출금()을 호출하면, 하나의 스레드가 계산 중일 때 다른 스레드가 잔액을 변경할 수 있습니다.
🤔 한 번에 하나의 스레드만 실행된다면?
만약 출금() 메서드가 한 번에 하나의 스레드만 실행되도록 제한된다면 어떨까요?
t1, t2가 동시에 출금()을 호출해도, t1이 먼저 끝까지 실행을 마친 후 t2가 실행된다면
balance는 중간에 바뀌지 않고, 출금 계산도 정확하게 됩니다.
여기서 중요한 점은, 잔액을 검증하는 순간부터 계산을 완료할 때까지 balance 값이 변경되지 않아야 한다는 것입니다.
이 구간을 여러 스레드가 동시에 접근하지 못하도록 막아야 합니다.
🌟 임계 영역 (Critical Section)
이처럼 동시에 접근하면 안 되는 중요한 코드 구간을 임계 영역(Critical Section)이라고 합니다.
주로 공유 자원(변수나 객체)을 읽거나 수정하는 부분이 해당됩니다. 앞서 살펴본 출금() 메서드는 전형적인 임계 영역입니다.
정확히는, 잔액을 검증하고 차감하는 과정 전체가 임계 영역이며, 이 구간은 반드시 한 번에 하나의 스레드만 접근해야 합니다.
여러가지 방법이 있지만 자바는 이런 임계 영역을 쉽게 보호할 수 있도록 synchronized 키워드를 제공합니다.
synchronized를 사용하면, 동시에 여러 스레드가 하나의 코드 블록에 진입하지 못하도록 막을 수 있습니다.
그렇다면 다음 단계에서는 synchronized를 활용해 출금 로직을 안전하게 만들어봅시다.
2. synchronized 메서드
자바의 synchronized 키워드를 사용하면 한 번에 하나의 스레드만 실행할 수 있는 코드 구간을 만들 수 있습니다.
@Override
public synchronized boolean withdraw(int amount) {...}
@Override
public synchronized int getBalance() {...}
- 이제 withdraw() , getBalance() 메서드는 한 번에 하나의 스레드만 실행할 수 있다.

실행 결과를 보면, t1이 withdraw() 메서드를 처음부터 끝까지 실행한 뒤에야 t2가 메서드에 진입해 실행합니다.
물론 환경에 따라 t2가 먼저 실행될 수도 있지만, 이 경우에도 t2가 전체 실행을 마쳐야 t1이 실행됩니다.
즉, 한 번에 하나의 스레드만 withdraw() 메서드를 실행하며, 두 스레드는 순차적으로 실행됩니다.
이것이 synchronized가 보장하는 동기화의 핵심입니다.
이제부터 자바의 synchronized 키워드가 어떻게 작동하는지 그림을 통해 단계별로 분석해보겠습니다.
또한 실행 중인 스레드 상태를 보면 t2가 BLOCKED 상태에 있는 것을 확인할 수 있는데, 이 상태가 의미하는 바도 함께 살펴봅시다.
🌟 모든 객체(인스턴스)는 내부에 고유한 락(lock)을 가지고 있습니다.
이를 모니터 락(monitor lock) 이라고 하며, 객체 내부에 존재하지만 외부에서 직접 확인할 수는 없습니다.
스레드가 synchronized가 붙은 메서드에 진입하려면, 반드시 해당 인스턴스의 락을 먼저 확보해야 합니다.
이번 예제에서는 BankAccount(x001) 인스턴스의 synchronized withdraw() 메서드를 호출하므로,
t1, t2 스레드는 모두 이 인스턴스의 락을 얻어야 메서드에 진입할 수 있습니다.
현재 t1, t2는 withdraw()를 실행하기 직전이며, 락을 가진 스레드만 먼저 실행됩니다.
- 스레드 t1이 먼저 synchronized가 붙은 withdraw() 메서드를 호출합니다.
- synchronized 메서드에 진입하려면, 먼저 해당 인스턴스의 락을 확보해야 합니다.
- t1은 락을 획득할 수 있었기 때문에, BankAccount(x001) 인스턴스의 메서드 실행을 시작합니다.
- 스레드 t1은 인스턴스의 락을 획득했기 때문에 withdraw() 메서드에 정상적으로 진입한다.
- 이때 스레드 t2도 같은 withdraw() 메서드 호출을 시도하지만, synchronized 메서드에 들어가려면 먼저 BankAccount(x001) 인스턴스의 락을 확보해야 한다.
- 하지만 락은 이미 t1이 가지고 있으므로, t2는 락을 얻지 못하고 BLOCKED 상태로 전환되어 대기하게 된다.
- 이 상태에서 t2는 락을 획득할 때까지 무기한 기다리며 실행되지 않는다.
참고로 BLOCKED 상태가 되면 락을 다시 획득하기 전까지는 계속 대기하고, CPU 실행 스케줄링에 들어가지 않습니다.
- t1: 출금을 위한 검증 로직을 수행한다. 조건을 만족하므로 검증 로직을 통과한다.
- 잔액[1000]이 출금액[800] 보다 많으므로 통과한다.
- t1 : 잔액 1000원에서 800원을 출금하고 계산 결과인 200원을 잔액(balance)에 반영한다.
- t1 : 메서드 호출이 끝나면 락을 반납한다.
- t2 : 인스턴스에 락이 반납되면 락 획득을 대기하는 스레드는 자동으로 락을 획득한다.
- 이때 락을 획득한 스레드는 BLOCKED → RUNNABLE 상태가 되고, 다시 코드를 실행한다.
- 스레드 t2 는 해당 인스턴스의 락을 획득했기 때문에 withdraw() 메서드에 진입할 수 있다.
- t2 : 출금을 위한 검증 로직을 수행한다. 조건을 만족하지 않으므로 false 를 반환한다.
- 이때 잔액(balance)은 200원이다. 800원을 출금해야 하므로 조건을 만족하지 않는다.
결국, t1은 800원 출금에 성공하고, t2는 잔액 부족으로 출금에 실패합니다. 최종 잔액은 1000원에서 200원이 되어, 정확하게 처리된 것을 확인할 수 있습니다. 이처럼 자바의 synchronized 를 사용하면 한 번에 하나의 스레드만 임계 영역을 실행하도록 보장할 수 있어, 공유 자원을 안전하게 보호할 수 있습니다.
참고 1: 락 획득 순서는 보장되지 않습니다.
BankAccount(x001)의 withdraw()를 여러 스레드가 동시에 호출하면, 하나의 스레드만 락을 획득하고 나머지는 BLOCKED 상태로 대기하게 됩니다. 이후 락이 반납되면, 대기 중인 스레드 중 하나가 락을 획득해 실행되는데, 어떤 스레드가 먼저 락을 얻을지는 자바에서 보장하지 않으며 실행 환경에 따라 달라질 수 있습니다. - (자바 표준에 정의되어 있지 않음)
참고 2: synchronized는 메모리 가시성도 보장합니다.
volatile을 사용하지 않아도, synchronized 블록이나 메서드 안에서 접근하는 변수는 메모리 가시성 문제 없이 안전하게 공유됩니다.
이는 synchronized가 락 획득 시 메인 메모리에서 값을 읽고, 락 해제 시 메인 메모리에 값을 반영하기 때문입니다.
3. synchronized 코드 블럭
synchronized의 가장 큰 장점이자 단점은 한 번에 하나의 스레드만 실행할 수 있다는 점입니다. 여러 스레드가 동시에 실행하지 못하므로 성능이 저하될 수 있습니다. 따라서 synchronized는 꼭 필요한 최소한의 범위에만 적용하는 것이 좋습니다. 공유 자원에 접근하거나 수정하는 임계 영역만 정확히 감싸는 것이 핵심입니다.
@Override
public synchronized boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
// ==임계 영역 시작==
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금에 걸리는 시간으로 가정(1초)
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
// ==임계 영역 종료==
log("거래 종료");
return true;
}
처음에 출력하는 "거래 시작"과 마지막에 출력하는 "거래 종료"는 공유 자원을 사용하지 않기 때문에 여러 스레드가 동시에 실행해도 전혀 문제가 되지 않습니다. 따라서 진짜 임계 영역은 잔액(balance)을 검증하고 차감하는 로직 부분입니다.
하지만 메서드 전체에 적용된 synchronized는 메서드 내 모든 코드가 한 번에 하나의 스레드만 실행되도록 제한합니다.
즉, 여러 스레드가 동시에 실행해도 문제가 없는 "거래 시작", "거래 종료" 출력 코드까지 불필요하게 순차 실행됩니다.
이를 해결하기 위해 자바는 synchronized를 메서드 단위가 아니라, 특정 코드 블록에만 적용할 수 있는 기능을 제공합니다.
이를 통해 임계 영역만 최소한으로 보호해 성능을 개선할 수 있습니다.
@Override
public boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
// ==임계 영역 시작==
synchronized (this) {
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
// 잔고가 출금액 보다 많으면, 진행.
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금에 걸리는 시간으로 가정(1초)
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
}
// ==임계 영역 종료==
log("거래 종료");
return true;
}
- 기존에 withdraw() 메서드 앞에 붙였던 synchronized를 제거합니다.
- 대신, synchronized(this) {} 코드 블록을 사용해 안전한 임계 영역을 직접 지정한다.
- 이렇게 하면 꼭 필요한 부분만 임계 영역으로 감싸서, 불필요한 동기화를 줄일 수 있습니다.
- synchronized(this)에서 괄호 안에 들어가는 값은 락을 획득할 객체 인스턴스의 참조입니다.
- 이번 예제에서는 BankAccountV3(x001) 인스턴스의 락을 사용하므로, 해당 인스턴스의 참조인 this를 넣어줍니다.
- 즉, 메서드 전체에 synchronized를 적용할 때와 같은 인스턴스 락을 확보하는 것입니다.
여기서 가장 중요한 점은, 한 번에 하나의 스레드만 실행할 수 있는 안전한 임계 영역은 가능한 최소한으로 제한해야 한다는 것입니다.
이렇게 하면 동시 실행 가능한 코드 구간이 늘어나 전체 처리 성능을 높일 수 있습니다.
이러한 동기화를 통해 다음 문제들을 해결할 수 있습니다.
- 경합 조건(Race condition) : 여러 스레드가 동시에 동일 자원을 수정할 때 발생하는 문제
- 데이터 일관성 : 여러 스레드가 동시에 접근하는 데이터의 일관성을 보장
동기화는 멀티스레드 환경에서 필수적이지만, 과도한 사용은 성능 저하를 초래할 수 있으므로 반드시 필요한 부분에만 적절히 적용해야 합니다. 마지막으로는 간략하게 synchronized의 장단점에 대해 정리해보겠습니다.
✅ synchronized 의 장점
- 언어 차원의 지원: 별도의 라이브러리 없이 문법으로 제공되어 사용이 간편함
- 자동 잠금 해제: 블록이나 메서드가 종료되면 자동으로 락 해제 → 개발자가 직접 해제하지 않아도 됨
- 간단하고 직관적: 기본적인 동기화 요구에는 충분히 대응 가능
⚠️ synchronized의 단점
- 무한 대기(BLOCKED): 락을 얻지 못한 스레드는 무한정 대기하며 → 타임아웃 없음, 중간 인터럽트 불가
- 공정성 미보장: 어떤 스레드가 락을 얻을지 예측 불가 → 특정 스레드가 영원히 락을 못 얻을 수도 있음
- 제어의 한계: 락 획득 대기 시간 설정, 우선순위 조절 등 세밀한 제어가 불가능
🚨 가장 큰 문제
synchronized는 락을 얻을 때까지 강제적으로 무한 대기합니다.
예를 들어 웹 요청 처리 중 락을 얻지 못하면, 사용자는 응답도 받지 못한 채 계속 대기하게 됩니다.
→ 오히려 일정 시간이 지나면 실패 응답을 주는 것이 사용자 경험에 더 낫습니다.
대안으로 복잡한 동시성 문제를 해결하기 위해 자바 1.5부터 java.util.concurrent 패키지 가 도입되어
더 세밀하고 유연한 동기화 기능(ReentrantLock, Semaphore, CountDownLatch 등)을 제공합니다.
📝 결론
- 간단한 동기화에는 synchronized가 여전히 유용
- 복잡한 동시성 제어에는 concurrent 패키지를 사용하는 것이 적합
'Java' 카테고리의 다른 글
[JAVA] 원자적 연산 (0) | 2025.06.26 |
---|---|
[JAVA] ReentrantLock (1) | 2025.06.24 |
[JAVA] happens-before 관계 (1) | 2025.06.20 |
[JAVA] volatile과 메모리 가시성 (0) | 2025.06.20 |
[JAVA] 스레드 제어와 생명 주기 (2) | 2025.06.19 |