Java21 은 Virtual Thread에서 carrier thread의 pinning 이슈를 유발해 성능을 저하시키는 synchronized 사용을 지양하고 ReentrantLock 으로 사용을 권장함
https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html
프로세스 내에서 실행되는 여러 흐름의 단위
같은 자원을 공유할 수 있기 때문에 동시에 여러 가지 일을 같은 자원을 두고 수행할 수 있고, 이는 여러 스레드가 동시에 하나의 자원을 공유하고 있기 때문에 자원 공유에서 동시성 이슈가 발생한다.
⇒ 동시성 문제를 해결하기 위한 여러 방법중 Lock 에 대해 알아보자
자바에서 멀티 스레드 환경에서 thread-safe 확보를 위해 synchronized
혹은 ReentrantLock
를 사용하는 방법이 대표적
synchronized
는 호출될 때 자동으로 Lock 작업을 수행한다. Lock 작업이 완료될때까지 내용은synchronized
는 모니터와 "대기 및 알림" 또는 "신호 및 계속" 메커니즘과 연결된 대기 세트를 사용하여 수행된다.
Java Monitor
- 여러 스레드가 객체로 동시에 객체로 접근하는 것을 막는다.
- 자바의 각 개체는 스레드를 잠그거나 잠금 해제할 수 있는 모니터와 연결되어 있다.
- 한 번에 하나의 Thread만 모니터에 대한 Lock을 보유할 수 있다. 해당 모니터를 잠그려고 시도하는 다른 스레드는 해당 모니터에 대한 잠금을 얻을 수 있을 때까지 차단된다.
1) 메서드에 Lock을 걸 때
class Test {
int count;
synchronized void bump() {
count++;
}
static int classCount;
static synchronized void classBump() {
classCount++;
}
}
2) 특정 변수에 Lock을 걸 때
class BumpTest {
int count;
void bump() {
synchronized (this) { count++; }
}
static int classCount;
static void classBump() {
try {
synchronized (Class.forName("BumpTest")) {
classCount++;
}
} catch (ClassNotFoundException e) {}
}
}
단, 변수에 lock을 걸기 위해선 해당 변수는 객체여야 한다. int, long과 같은 기본형 타입에는 lock을 걸 수 없다.
wait() : 객체의 lock을 풀고 Thread를 해당 객체의 waiting pool 에 넣는다.
notify() : waiting pool 에서 대기중인 Thread 중 하나를 깨운다.
notifyAll() : waiting pool 에서 대기중인 모든 Thread를 깨운다.
synchronized 로직은 JDK 내에 내장 되어 있고, C++ 언어로 구현이 되어있다. [오픈소스]
⭐️ notify()를 실행할때, 대기 중인 스레드(waitSet) 중에서 어떤 스레드가 선택되는지 보장할 수 없다.
⇒ 어느 부분이 Lock인지 알수 없다, 암시적 Lock
java.util.concurrent
패키지에 포함되어있다.synchronized
과 동일한 의미와 기본동작을 갖지만 명시적으로 Lock 객체를 생성하며 스레드의 재진입성을 지원하는 더 확장된 Lock 이다.ReentrantLock이 많은 기능들을 제공함을 알 수 있다.
CAS(Comapre And Swap)으로 동시에 하나의 스레드만이 잠금을 획득할 수 있도록 상태값을 관리하여 스레드 안전을 보장합니다.
경쟁에 실패해 대기중인 스레드는 작업이 일시 중단된 후 FIFO 대기열에 저장이 된다.
동일한 스레드에 의해 최대 20억번 까지의 재귀적 잠금을 지원한다.
직접적으로 Lock 객체를 생성하여 사용할 수 있습니다.
public class ReentrantLock
extends Object
implements Lock, Serializable
잠금 및 잠금 해제는 모두 수동으로 처리할 수 있다.
잠금 시간 제한을 설정할 수 있다. 만료가 되면 해당 스레드를 건너뛰기 때문에 synchronized
에서 발생할 수 있는 교착 상태를 방지할 수 있다.
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}