Java Lock 예제 – ReentrantLock

자바 락 예제 튜토리얼에 오신 것을 환영합니다. 일반적으로 멀티스레드 환경에서 작업할 때, 스레드 안전성을 위해 동기화를 사용합니다.

자바 락

대부분의 경우, 동기화 키워드가 가장 적합하지만, 일부 단점들로 인해 자바 동시성 패키지의 Lock API가 도입되었습니다. 자바 1.5 동시성 API에서는 java.util.concurrent.locks 패키지와 함께 Lock 인터페이스 및 일부 구현 클래스가 제공되어 객체 락 메커니즘을 개선할 수 있습니다. 자바 락 API에서 중요한 인터페이스와 클래스 몇 가지는 다음과 같습니다:

  1. Lock: 이는 락 API의 기본 인터페이스입니다. 동기화 키워드의 모든 기능을 제공하며, 락을 위해 다른 조건을 생성하고, 스레드가 락을 기다리도록 대기 시간을 설정하는 추가 방법을 제공합니다. 중요한 몇 가지 메서드는 락을 획득하기 위한 lock(), 락을 해제하기 위한 unlock(), 특정 기간 동안 락을 기다리기 위한 tryLock(), 조건을 생성하기 위한 newCondition() 등입니다.

  2. 조건: 조건 객체는 대기(wait)-통지(notify) 모델과 유사하지만 대기 집합을 만드는 추가 기능이 있습니다. 조건 객체는 항상 잠금 객체에 의해 생성됩니다. 중요한 메서드 중 일부는 wait()와 유사한 await() 및 notify()와 유사한 signal(), notifyAll() 메서드입니다.

  3. 읽기-쓰기 잠금: 읽기 전용 작업 및 쓰기 작업을 위한 연관된 두 개의 잠금이 포함되어 있습니다. 읽기 잠금은 작성자 스레드가 없는 한 여러 리더 스레드에 의해 동시에 보유될 수 있습니다. 쓰기 잠금은 배타적입니다.

  4. 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 락과 동기화 사이의 다음 차이점을 쉽게 결론 내릴 수 있습니다.

  1. Java 락 API는 락에 대한 더 많은 가시성과 옵션을 제공합니다. 동기화에서는 스레드가 락을 무기한 대기할 수 있지만 tryLock()를 사용하여 스레드가 특정 시간만 기다리도록 할 수 있습니다.
  2. 동기화 코드는 훨씬 더 깨끗하고 유지보수가 쉽지만, 락을 사용하면 lock()와 unlock() 메서드 호출 사이에 예외가 발생해도 락이 해제되도록 강제됩니다.
  3. 동기화 블록이나 메서드는 하나의 메서드만 커버할 수 있지만 Lock API를 사용하여 한 메서드에서 잠금을 얻고 다른 메서드에서 잠금을 해제할 수 있습니다.
  4. synchronized 키워드는 공정성을 제공하지 않지만 ReentrantLock 객체를 생성할 때 공정성을 true로 설정하여 가장 긴 대기 스레드가 먼저 잠금을 얻을 수 있도록 할 수 있습니다.
  5. Lock에 대한 다른 조건을 생성하고 다른 스레드가 다른 조건을 기다릴 수 있습니다.

이것이 자바 락 예제, 자바에서의 ReentrantLock 및 synchronized 키워드와의 비교 분석입니다.

Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock