쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화

2022. 5. 8. 18:58·자바/프로세스와 쓰레드

1. Lock 클래스

- synchronized 블럭 외에도 'java.util.concurrent.locks' 패키지가 제공하는 lock 클래스들을 

이용하는 방법이 있다.

 

- synchronized 블럭으로 동기화를 하면 자동적으로 lock이 잠기고 풀리며 심지어 예외가

발생하여도 lock은 자동으로 풀린다. 

 

- 그러나, 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 있어서 불편할 때가 있다.

 

 

- 그럴 때 아래와 같은 lock 클래스의 종류 3가지를 사용한다.

클래스 설명
ReentrantLcok 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가.

 

( ReentrantLcok이 가장 일반적인 lock이다. )

( Lock 앞에 Reentrant이 붙은 이유는 앞서 wait()&notify()에서 배운 것처럼, 특정 조건에서 lock을 풀고

나중에 다시 lock을 얻어 CS에 들어와서 작업할 수 있기 때문이다. )

 

( ReentrantReadWriteLock은 읽고 쓸 때 lock을 건다. )

( 한 쓰레드가 읽기 lock을 쓰고 있을 때, 다른 쓰레드가 중복해서 lock을 중복해서 읽기를 할 수 있다. )

( 읽기는 내용을 변경하지 않으므로 상관없지만, 쓰기같은 경우는 중복허용을 하지 않는다. )

( 또한 읽기의 lock이 걸려있는 상태에서 쓰기 lock을 걸 수 없다. 반대의 경우도 마찬가지 )

 

( StampedLock는 lock을 걸거나 해지할 때 '스탬프(long 타입의 정수값)'을 사용하며, 읽기와 쓰기 lock외에

낙관적 읽기가 추가된 것이다. )

( 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하는데 비해

낙관적 읽기 lock은 쓰기 lock에 의해 바로 풀린다. )

( 그래서 낙관적 읽기에 실패하면 다시 읽기 lock을 읽어와야 한다. )

( 그러므로, 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기에 충돌할 때만 쓰기가 끝난 후에 읽기 lock을

거는 것이다. )

 

 

( 아래는 가장 일반적인 StampedLock을 이용한 낙관적 읽기의 예이다. )

int getBalance(){
    long stamp = lock.tryOptimisticRead();	//낙관적 읽기 lock을 건다.
    int curBalance = this.balance;			//공유 데이터인 balance를 읽어온다.
    
    if(!lock.validate(stamp)){				//쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
    	stamp = lock.readLock();			//lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.
        
        try{
	    curBalance = this.balance		//공유데이터를 다시 읽어온다.
        } finally{
            lock.unlockRead(stamp);			//읽기 lock을 푼다.
        }
    }
    
    return curBalance;
}

 

 

 

 

1-1. ReetrantLock의 생성자

- ReentrantLock의 생성자는 아래와 같이 두 개의 생성자를 가지고 있다.

 

ReentrantLock()

ReentrantLock(boolean fair)

 

( 만약 fair에 true로 주면 lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock 획득할 수 있게, 즉 공정하게

처리한다. )

( 그러나 공정하게 처리한다면 성능이 떨어질 수 밖에 없다. )

( 대부분은 공정하게 처리하지 않아도 별 문제가 생기지 않기 때문에 성능을 선택하는 것이 좋다. )

 

 

- ReentrantLock은 synchronized 블럭과 달리 수동으로 아래와 같이 잠그고 해제해야 한다. )

 

lock.lock();

try{

    //CS

} finally{

    lock.unlock();

}

 

( 위 try-finally를 쓴 이유는 CS에서 예외가 발생하거나 return 문으로 빠져 나가게 되면 lock이 풀리지

않을 수 있으므로 unlock()을 꼭 finally에 쓰는 것이 좋다. )

 

 

- 또 한 가지 tryLock()이 있는데, 지정된 시간만큼 기다린다. 만약 얻지 못한다면 false, 얻으면 true를

반환한다.

 

boolean tryLock()

boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

 

( lock()은 lock을 얻기 위해서는 쓰레드를 블락시키므로, 응답성이 나빠질 수 있다. )

( 그러므로, 응답성을 중요시 여기는 프로그램은 tryLock()을 적절히 사용하여 일정시간동 기다린다음

포기하도록 결정할 수 있게 사용할 수 있다. )

( 시간이 지나면 interrupt()에 의해 취소된다. )

 

 

 

 

1-2. ReentrantLock과 Condition

- 앞서 wait()&notify() 예제에서 요리사 쓰레드와 손님 쓰레드를 구분해서 통지하는 것을 구현하지 

못하였다.

 

- 그러나 Condition은 이 문제점을 손님 쓰레드를 위한 Condition과 요리사 쓰레드를 위한 Condition을

만들어서 각각의 waiting pool에서 따로 기다리도록 하면 문제는 해결할 수 있다.

 

 

- Condition은 이미 생성된 lock으로부터 newCondition()을 호출해서 아래와 같이 생성한다.

private ReentrantLock lock = new ReentrantLock(); //lock 생성

//lock으로 condition 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();

 

( 또한 wait()&notify() 대신 Condition의 await()&signal()을 사용하면 된다. )

 

Object Condition
void wait() void await()
void awaitUninterruptedtibly()
void wait(long timeout) boolean await(long time, TimeUnit unit)
long awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline)
void notify() void signal()
void notifyAll() void signalAll()

 

 

 

 

1-2-1.ReentrantLock과 Condition을 이해하기 위한 예제(1)

: 앞서 wait()&notify()에서 구분해서 통지못한 것을 await()&signal로 해결한 예제이다.

 

import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Exercise020 {
	public static void  main(String[] args) throws Exception {
		Table3 table3 = new Table3();
		
		new Thread(new Cook3(table3), "요리사").start();
		new Thread(new Customer3(table3, "donut"),  "손님 1").start();
		new Thread(new Customer3(table3, "burger"), "손님 2").start();
		
		//3초 뒤에 프로그램을 종료시킨다.
		Thread.sleep(3000);
		System.exit(0);
	}
}

class Table3 {
	String[] dishNames = { "donut","burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();

	private ReentrantLock lock = new ReentrantLock();
	private Condition forCook = lock.newCondition();
	private Condition forCust  = lock.newCondition();

	public void add(String dish) {
		lock.lock();

		try {
			while(dishes.size() >= MAX_FOOD) {
					String name = Thread.currentThread().getName();
					System.out.println(name+" is waiting.");
					try {
						forCook.await(); // wait(); 요리사 쓰레드를 기다리게 한다.
						Thread.sleep(500);
					} catch(InterruptedException e) {}	
			}

			dishes.add(dish);
			forCust.signal(); // notify(); 손님 깨운다.
			System.out.println("Dishes:" + dishes.toString());
		} finally {
			lock.unlock();
		}
	}

	public void remove(String dishName) {
		lock.lock(); //		synchronized(this) {	
		String name = Thread.currentThread().getName();

		try {
			while(dishes.size()==0) {
					System.out.println(name+" is waiting.");
					try {
						forCust.await(); // wait(); 손님 쓰레드를 기다리게 한다.
						Thread.sleep(500);
					} catch(InterruptedException e) {}	
			}

			while(true) {
				for(int i=0; i<dishes.size();i++) {
					if(dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						forCook.signal(); // notify();잠자고 있는 COOK을 깨움
						return;
					}
				} // for문의 끝

				try {
					System.out.println(name+" is waiting.");
					forCust.await(); // wait(); // CUST쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}	
			} // while(true)
			 // } // synchronized
		} finally {
			lock.unlock();
		}
	}

	public int dishNum() { return dishNames.length; }
}

class Cook3 implements Runnable {
	private Table3 table;
	
	Cook3(Table3 table) {	this.table = table; }

	public void run() {
		while(true) {
			int idx = (int)(Math.random()*table.dishNum());
			table.add(table.dishNames[idx]);
			try { Thread.sleep(10);} catch(InterruptedException e) {}
		} // while
	}
}

class Customer3 implements Runnable {
	private Table3 table;
	private String food;

	Customer3(Table3 table, String food) {
		this.table = table;  
		this.food  = food;
	}

	public void run() {
		while(true) {
			try { Thread.sleep(100);} catch(InterruptedException e) {}
			String name = Thread.currentThread().getName();
			
			table.remove(food);
			System.out.println(name + " ate a " + food);
		} // while
	}
}

 

( 구분하여 통지한 결과 '기아 현상'과 '경쟁 상태'가 확실히 개선되게 되었다. )

( 그러나 쓰레드의 종류에 따라 구분지어 통지하더라도 여전히 같은 종류의 쓰레드끼리는 '기아 현상'과 '경쟁 상태'를

해결하지 못하였다. )

 

( 그 해결책은 손님에서 더 세분화하여 Condition을 만드는 것이다. )

 

( 아래의 링크에서 1-3 예제(3)이랑 비교를 통해 어떤 것이 달라졌는지 확인하자. )

쓰레드의 동기화(2): wait() & notify() (tistory.com)

 

쓰레드의 동기화(2): wait() & notify()

1. wait()와 notify() - 동기화도 중요하지만, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것 또한 중요하다. - 이러한 것을 구현하기 위해 wait()와 notify()가 등장하게 되

kind-coding.tistory.com

 

저작자표시 (새창열림)

'자바 > 프로세스와 쓰레드' 카테고리의 다른 글

fork & join 프레임웍  (0) 2022.05.10
volatile  (0) 2022.05.10
쓰레드의 동기화(2): wait() & notify()  (0) 2022.05.07
쓰레드의 동기화(1): Critical Section & Lock, snychronized  (0) 2022.05.07
쓰레드의 실행제어  (0) 2022.02.20
'자바/프로세스와 쓰레드' 카테고리의 다른 글
  • fork & join 프레임웍
  • volatile
  • 쓰레드의 동기화(2): wait() & notify()
  • 쓰레드의 동기화(1): Critical Section & Lock, snychronized
백_곰
백_곰
  • 백_곰
    친절한 코딩
    백_곰
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 알고리즘 (with JAVA)
        • 기본 알고리즘
        • 완전 탐색
        • 분할 정복 알고리즘
        • 동적 계획법
        • 탐욕법
        • 코딩 테스트 기출 문제
        • 코드트리 조별과제
      • 백준 (with JAVA)
        • 완전 탐색
        • 분할 정복
        • 그 외
      • 자바
        • 개발 환경 구축하기
        • 팁
        • 기본적인 개념
        • 컬렉션 프레임워크
        • 프로세스와 쓰레드
        • 지네릭스
        • 람다식
        • 스트림
        • 입출력 IO
        • 네트워킹
        • 열거형(enums)
        • java.lang 패키지
        • java.time 패키지
        • 유용한 클래스들
        • 형식화 클래스들
      • 안드로이드 with 자바
        • 응용 문제들
        • 자잘한 문제들
        • 오류 보고서
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      스트림
      Collections Framework
      코딩테스트
      snail
      문자 기반 스트림
      불안정 정렬
      제자리 정렬
      TCP 소켓 프로그래밍
      다형성
      InputStream
      Arrays
      코드트리
      안드로이드 스튜디오
      outputstream
      유용한 클래스
      자바 개념
      ServerSocket
      역직렬화
      map()
      알고스팟
      file
      선택 정렬
      serializable
      소켓 프로그래밍
      java.time 패키지
      안정 정렬
      람다식
      코딩트리조별과제
      중간연산
      java.lang패키지
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    백_곰
    쓰레드의 동기화(3): Lock과 Condition을 이용한 동기화
    상단으로

    티스토리툴바