동기화(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 같은 명시적인 락에서도 똑같이 적용된다.
참고
https://mint723dev.tistory.com/71
고유 락 (Intrinsic Lock)
자바의 모든 객체는 락(lock)을 갖고 있다모든 객체가 갖고 있으므로 고유 락(intrinsic lock)이라고 하며, 모니터처럼 동작한다고 하여 모니터 락(monitor lock), 혹은 그냥 모니터(monitor)라고 한다 예제
mint723dev.tistory.com
https://velog.io/@tkdtkd97/Java-%EA%B3%A0%EC%9C%A0-%EB%9D%BD-Intrinsic-Lock
Java 고유 락 (Intrinsic Lock)
자바의 모든 객체는 락(lock)을 갖고 있다.모든 객체가 갖고 있으니 고유 락(intrinsic lock), 모니터처럼 동작한다고 하여 모니터 락(monitor lock), 혹은 그냥 모니터(monitor)라고도 한다.동시성 문제를 해
velog.io
[읽고서] 자바 고유락과 Synchronization
기술 블로그 리뷰 | Java는 크게 3가지 영역의 메모리 영역을 가지고 있습니다. static 영역 Java 클래스 파일은 크게 필드(field), 생성자(constructor), 메소드(method)로 구성됩니다. 그중 필드 부분에서 선
brunch.co.kr
'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 |