자바/프로세스와 쓰레드

쓰레드의 동기화(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