동기화(synchronization)와 락(Lock)
Java에서 동기화는 여러 스레드가 공유 자원에 동시 접근할 때 발생할수 있는 문제를 방지하는데 사용된다.
Lock은 동기화를 위한 방법 중 하나로, 데이터의 일관성과 무결성을 유지할수 있다.
Lock 의 목적
- 상호 배제(Mutual Exclusion) : 한번에 하나의 스레드만 자원에 접근할 수 있도록 보장
- 교착 상태(Deadlock) 방지 : 올바른 락 획득 순서를 유지하여 교착상태 방지
- 경쟁 상태 방지 : 여러 스레드가 동시에 자원에 접근하여 발생하는 예기치 않은 결과를 방지
고유 락(Intrinsic Lock)
Java에서 모든 객체는 하나의 고유 락을 가지고 있다.
이는`synchronized` 키워드를 사용할 때 자동으로 사용된다.
public class IntrinsicLockExample {
// 동기화된 메서드
public synchronized void synchronizedMethod() {
System.out.println(Thread.currentThread().getName() + ": Enter synchronizedMethod");
// 간단한 작업 시뮬레이션
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": Exit synchronizedMethod");
}
public static void main(String[] args) {
IntrinsicLockExample example = new IntrinsicLockExample();
// 두 개의 스레드를 생성하여 같은 메서드를 호출
Thread thread1 = new Thread(() -> example.synchronizedMethod(), "Thread-1");
Thread thread2 = new Thread(() -> example.synchronizedMethod(), "Thread-2");
thread1.start();
thread2.start();
}
}
`synchronizedMethod`는 `synchronozed` 키워드로 동기화 되어 있다.
Thread-1 과 Thread-2가 동시에 `synchronizedMethod`를 호출하려고 할 때,
Thread-1이 먼저 고유 락을 획득하고, `synchronizedMethod`에 들어간다.
Thread-2는 Thread-1이 고유 락을 해제할 때까지 대기한다.
Thread-1이 메서드를 종료하고 락을 해제하면, Thread-2가 고유락을 획득하고 메서드에 들어간다.
고유락 재진입 (Reentrancy)
한 스레드가 이미 특정 객체의 고유락을 획득하고 있는 상태에서,
다시 그 고유락을 필요로 하는 `synchronized`블록이나 메서드에 진입하려고 할 때,
해당 스레드는 블록되지 않고 정상적으로 진입할 수 있다.
public class ReentrantLockExample {
// synchronized 메서드
public synchronized void outerMethod() {
System.out.println(Thread.currentThread().getName() + ": Enter outerMethod");
innerMethod(); // 동일한 객체의 다른 synchronized 메서드 호출
}
// synchronized 메서드
public synchronized void innerMethod() {
System.out.println(Thread.currentThread().getName() + ": Enter innerMethod");
// 추가적인 작업 시뮬레이션
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": Exit innerMethod");
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread thread = new Thread(example::outerMethod, "Thread-1");
thread.start();
}
}
Thread-1이 `outerMethod`를 호출한다. 이는 `synchronized`메서드로, `ReentrantLockExample` 객체의 고유 락을 획득한다.
`outerMethod`안에서 `innerMethod`를 호출한다. 이것도 `synchronized`메서드이므로, 동일한 객체의 고유락을 필요로 한다.
이때, Thread-1은 이미 `ReentrantLockExample` 객체의 락을 획득했기 때문에, 별다른 과정 없이 `innerMethod`에 진입할 수 있다.
만약 재진입을 허용하지 않는다면 어떻게 될까?
스레드가 이미 락을 가지고 있는상태에서 다시 락을 획득하려고 할 때
자신이 소유한 락을 획득하려고 시도하면서 무한히 대기하게 되므로 교착상태에 빠질 수 있다.
위의 코드에서 `innerMethod`의 Lock이 해제되기 전에 `outerMethod`의 Lock을 해제할 수 있을까?
재진입 락의 특성때문에 innerMethod의 락이 해제되기 전에는 outerMethod의 락을 해제할 수 없다.
구조적 락(Structured Lock)
구조적 락(구조적 동기화)은 락을 얻고 해제하는 범위가 명확히 정의되어 이쓴 방식이다.
예시와 같이 구조적 락은 보통 `synchronized` 키워드로 구현되며 자동으로 락의 획득과 해제를 관리한다.
public class StructuredLockDemo {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void demonstrateStructuredLock() {
synchronized (lockA) {
System.out.println("Lock A acquired");
synchronized (lockB) {
System.out.println("Lock B acquired");
// 여기서 lockB를 먼저 해제해야 함
} // Lock B 해제
// Lock A는 블록의 마지막에 자동으로 해제됨
} // Lock A 해제
}
}
위와 같은 예시에서 블록 단위로 lock의 획득 및 해제가 일어나므로
`A` 획득 -> `B`획득 -> `B`해제 -> `A`해제는 가능하지만
`A` 획득 -> `B`획득 -> `A`해제 -> `B`해제는 불가능하다.
이를 가능하게 하기 위해서는 명시적 락을 사용하면 된다.
명시적 락(Explicit Lock)
명시적 락은 `Lock` 인터페이스로 구현되며, 개발자가 직접 락의 획득과 해제를 관리해야 한다.
아래는 명시적 락 중 대표적인 `ReentrantLock` 클래스에 대한 예제이다.
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lockA = new ReentrantLock();
private final ReentrantLock lockB = new ReentrantLock();
public void demonstrateReentrantLock() {
lockA.lock();
try {
System.out.println("Lock A acquired");
lockB.lock();
try {
System.out.println("Lock B acquired");
} finally {
lockA.unlock(); // 먼저 획득한 A 락 먼저 해제
}
} finally {
lockB.unlock(); // 나중에 획득한 B 락 나중에 해제
}
}
}
`lockA.lock()`을 통해 락을 획득한다.
ㄴ`lockA`를 성공적으로 획득하면 다른 스레드는 `lockA`에 대한 락을 획득할 수 없다.
`lockA`를 획득한 후 `lockB.lock()`을 통해 락을 획득한다.
ㄴ`lockB`도 마찬가지로 다른 스레드는 `lockB`에 대한 락을 획득할 수 없다.
`lockA.unlock()`을 통해 `lockA`에 대한 락을 `lockB`보다 먼저 해제한다.
이 후, `lockB.unlock()`을 통해 `lockB`에 대한 락을 해제한다.
명시적 락의 주요 메서드
- `lock()` : 락을 획득할 때까지 현재 스레드 대기
- `lockInterrupteibly()` : 인터럽트가 가능한 방식으로 락 획득
- `tryLock()` : 락을 시도하고 즉시 성공하면 true, 아니면 false 반환
- `tryLock(long time, TimeUnit unit)` : 주어진 시간내에 락을 시도하고 성공하면 true, 아니면 false 반환
- `unlock()` : 락을 해제
💡 주요 명시적 락 클래스
- ReentrantLock : 재진입 가능한 락을 제공. 동일한 스레드가 여러번 락 획득할 수 있음
- ReadWriteLock : 읽기/쓰기 작업을 구분하여 여러 스레드가 동시에 읽기 작업을 수행할 수 있도록 함
- StampedLock : ReadWriteLock의 대안으로 낙관적 읽기 잠금을 제공. 더 높은 성능.
가시성(Visibility)
동시성 프로그램의 이슈 중 하나는 가시성이다. 값을 사용한 다음 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있게 해야 한다는 의미이다.
자바에서는 스레드가 락을 획득하는 경우 그 이전에 쓰였던 값들의 가시성을 보장한다.
이는 고유 락 뿐만 아니라 ReentrantLock 같은 명시적인 락에서도 똑같이 적용된다.
'Language > Java' 카테고리의 다른 글
record 클래스 (Java 14) (0) | 2024.12.13 |
---|---|
직렬화(Serialization) (0) | 2024.12.12 |
Java의 스레드(Thread) (0) | 2024.12.10 |
Error & Exception (1) | 2024.12.09 |
가비지 컬렉션 (GC; Garbage Collection) (0) | 2024.12.08 |