자바 락 예제 튜토리얼에 오신 것을 환영합니다. 일반적으로 멀티스레드 환경에서 작업할 때, 스레드 안전성을 위해 동기화를 사용합니다.
자바 락
대부분의 경우, 동기화 키워드가 가장 적합하지만, 일부 단점들로 인해 자바 동시성 패키지의 Lock API가 도입되었습니다. 자바 1.5 동시성 API에서는
java.util.concurrent.locks
패키지와 함께 Lock
인터페이스 및 일부 구현 클래스가 제공되어 객체 락 메커니즘을 개선할 수 있습니다. 자바 락 API에서 중요한 인터페이스와 클래스 몇 가지는 다음과 같습니다:
-
Lock: 이는 락 API의 기본 인터페이스입니다. 동기화 키워드의 모든 기능을 제공하며, 락을 위해 다른 조건을 생성하고, 스레드가 락을 기다리도록 대기 시간을 설정하는 추가 방법을 제공합니다. 중요한 몇 가지 메서드는 락을 획득하기 위한 lock(), 락을 해제하기 위한 unlock(), 특정 기간 동안 락을 기다리기 위한 tryLock(), 조건을 생성하기 위한 newCondition() 등입니다.
-
조건: 조건 객체는 대기(wait)-통지(notify) 모델과 유사하지만 대기 집합을 만드는 추가 기능이 있습니다. 조건 객체는 항상 잠금 객체에 의해 생성됩니다. 중요한 메서드 중 일부는 wait()와 유사한 await() 및 notify()와 유사한 signal(), notifyAll() 메서드입니다.
-
읽기-쓰기 잠금: 읽기 전용 작업 및 쓰기 작업을 위한 연관된 두 개의 잠금이 포함되어 있습니다. 읽기 잠금은 작성자 스레드가 없는 한 여러 리더 스레드에 의해 동시에 보유될 수 있습니다. 쓰기 잠금은 배타적입니다.
-
ReentrantLock: Lock 인터페이스의 가장 널리 사용되는 구현 클래스입니다. 이 클래스는 synchronized 키워드와 유사한 방식으로 Lock 인터페이스를 구현합니다. Lock 인터페이스 구현 외에도 ReentrantLock에는 락을 보유한 스레드, 락을 획득하려는 스레드 등을 얻기 위한 몇 가지 유틸리티 메서드가 포함되어 있습니다. synchronized 블록은 재진입이 가능하며, 즉, 스레드가 모니터 객체에 락을 가지고 있고 다른 synchronized 블록이 동일한 모니터 객체에 락을 획득하려고 하면 해당 코드 블록에 진입할 수 있습니다. 클래스 이름이 ReentrantLock인 이유로 생각합니다. 이 기능을 간단한 예제로 이해해 봅시다.
public class Test{ public synchronized foo(){ //do something bar(); } public synchronized bar(){ //do some more } }
스레드가 foo()에 진입하면 Test 객체에 락이 있으므로 bar() 메서드를 실행하려고 할 때 이미 Test 객체에 락을 보유하고 있으므로 스레드는 bar() 메서드를 실행할 수 있습니다. 즉, synchronized(this)와 동일합니다.
자바 락 예제 – 자바에서의 ReentrantLock
이제 synchronized 키워드를 Java Lock API로 대체하는 간단한 예제를 살펴보겠습니다. 스레드 안전이 보장되어야 하는 작업이 있는 Resource 클래스와 스레드 안전이 필요하지 않은 몇 가지 메서드가 있는 경우를 가정해 봅시다.
package com.journaldev.threads.lock;
public class Resource {
public void doSomething(){
//작업 수행, DB 읽기, 쓰기 등
}
public void doLogging(){
//로그 기록, 스레드 안전이 필요하지 않음
}
}
지금 Runnable 클래스가 있다고 가정해 보겠습니다. 여기서는 리소스 메서드를 사용할 것입니다.
package com.journaldev.threads.lock;
public class SynchronizedLockExample implements Runnable{
private Resource resource;
public SynchronizedLockExample(Resource r){
this.resource = r;
}
@Override
public void run() {
synchronized (resource) {
resource.doSomething();
}
resource.doLogging();
}
}
리소스 객체에 대한 락을 얻기 위해 동기화된 블록을 사용하고 있음을 주목하세요. 클래스에 더미 객체를 만들어서 그것을 잠금용으로 사용할 수도 있었습니다. 이제 Java 락 API를 사용하여 위 프로그램을 동기화된 키워드를 사용하지 않고 다시 작성하는 방법을 살펴보겠습니다. Java에서 ReentrantLock을 사용할 것입니다.
package com.journaldev.threads.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyLockExample implements Runnable{
private Resource resource;
private Lock lock;
public ConcurrencyLockExample(Resource r){
this.resource = r;
this.lock = new ReentrantLock();
}
@Override
public void run() {
try {
if(lock.tryLock(10, TimeUnit.SECONDS)){
resource.doSomething();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
//락 해제
lock.unlock();
}
resource.doLogging();
}
}
보시다시피, tryLock() 메서드를 사용하여 스레드가 정해진 시간 동안만 기다리도록 하고, 객체에 대한 락을 얻지 못하면 로깅하고 종료합니다. 또 다른 중요한 점은 try-finally 블록을 사용하여 doSomething() 메서드 호출이 예외를 던지더라도 락이 반드시 해제되도록 하는 것입니다.
Java 락 vs 동기화
위 세부 정보와 프로그램을 기반으로 Java 락과 동기화 사이의 다음 차이점을 쉽게 결론 내릴 수 있습니다.
- Java 락 API는 락에 대한 더 많은 가시성과 옵션을 제공합니다. 동기화에서는 스레드가 락을 무기한 대기할 수 있지만 tryLock()를 사용하여 스레드가 특정 시간만 기다리도록 할 수 있습니다.
- 동기화 코드는 훨씬 더 깨끗하고 유지보수가 쉽지만, 락을 사용하면 lock()와 unlock() 메서드 호출 사이에 예외가 발생해도 락이 해제되도록 강제됩니다.
- 동기화 블록이나 메서드는 하나의 메서드만 커버할 수 있지만 Lock API를 사용하여 한 메서드에서 잠금을 얻고 다른 메서드에서 잠금을 해제할 수 있습니다.
- synchronized 키워드는 공정성을 제공하지 않지만 ReentrantLock 객체를 생성할 때 공정성을 true로 설정하여 가장 긴 대기 스레드가 먼저 잠금을 얻을 수 있도록 할 수 있습니다.
- Lock에 대한 다른 조건을 생성하고 다른 스레드가 다른 조건을 기다릴 수 있습니다.
이것이 자바 락 예제, 자바에서의 ReentrantLock 및 synchronized 키워드와의 비교 분석입니다.
Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock