Lock과 Condition을 이용한 동기화
동기화 할 수 있는 방법은 synchronized블럭 외에도 java.util.concurrent.locks패키지가 제공하는 lock클래스들을
이용하는 방법이 있다.
lock 클래스의 종류는 다음과 같다.
ReentrantLock // 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock //읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock //ReentrantReadWriteLock 에 낙관적인 lock의 기능을 추가
ReentrantLock 은 가장 일반적인 lock이다. reentrant(재진입할 수 있는)이라는 단어가 앞에 붙은 이유는
우리가 앞서 wait(), notify()에서 배운것 처럼, 특정 조건에서는 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로
들어와서 이후의 작업을 수행할 수 있기 때문이다. 지금까지 우리가 lock이라고 불러왔던 것과 일치한다.
ReentrantReadWriteLock 읽기를 위한 lock과 쓰기를 위한 lock을 제공한다. 읽기 lock이 걸려있으면, 다른 쓰레드가
읽기 lock을 중복해서 걸고 읽기를 수행할 수 있다. 읽기는 내용을 변경하지 않으므로, 동시에 여러 쓰레드가 읽어도
문제가 되지 않는다. 그러나 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않는다.
반대의 경우도 마찬가지다. 읽기의 할때는 읽기 lock 을 걸고, 쓰기를 할때는 쓰기 lock을 거는 것일 뿐 lock을 거는건 같다.
StampedLock lock을 걸거나 해지할 때 스탬프(long 타입의 정수값)를 사용하며, 읽기와 쓰기를 위한 lock외에 '낙관적읽기 lock (optimistic reading lock)'이 추가된 것이다.
읽기 lock이 걸려있으면, 쓰기 lock을 얻기위해 읽기 lock이 풀릴 때까지 기다려야 하는데 비해, lock을 얻어서 다시 읽어 와야 한다.
무조건 읽기 lock을 걸지않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.
다음의 코드는 가장 일반적인 StampedLock을 이용한 낙관적 읽기의 예이다.
int getBlance() { // 낙관적 읽기 lock을 건다.
long stamp = lock.tryOptimisticRead(); //공유데이터인 balance를 읽어온다.
int curBalance = this.balance; //쓰기 lock에 의한 낙관적 읽기 lock이 풀렸는지 확인
if(!lock.validate(stamp)) { //lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.
stamp = lock.readLock();
try {
curBalance = this.balance(); //공유데이터를 다시 읽어온다.
}finally{
lock.unlockRead(stamp); //읽기 lock을 푼다.
}
}
return curBalance; //낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
}
대강 Lock의 생성자들이 있다는것을 알았으니 한번 알아보자
ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair)
생성자의 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득할 수 있게, 즉 공정하게 처리한다.
그러나 공정하게 처리하려면 어떤 쓰레드가 가장 오래 걸렸는지 확인하는 과정을 거칠 수 밖에없으므로, 성능이
일부 저하될수있다. - 성능이 떨어진다.
void lock() //lock을 잠근다.
void unlock //lock을 해지한다.
boolaen isLocked() //lock이 잠겼는지 확인한다.
자동적으로 lock의잠금 해제가 관리되는 synchronized블럭과 달리, ReentrantLock과 같은 lock클래스들은 수동으로
lock을 잠그고 해제해야한다. 그래도 lock을 잠구고 푸는 것은 간단하다.
그저 매서드를 호출하기만 하면된다.
lock을 걸고 나서 푸는 것을 잊어버리지 않아아한다.
임계 영역 내에서 예외가 발생하거나 return문으로 빠져나가게 되면 lock이 풀리지 않을 수 있으므로, unlock()은 try-finally문으로 감싸는 것이 일반적이다.
lock.lock()
try{
//임계영역
}finally{
lock.unlock();
}
이렇게 하면 try블럭 내에서 어떤 일이 발생해도 finally블럭에 있는 unlock()이 수행되어 lock이 풀리지 않는 일은
발생하지 않는다. 대부분의 경우 lock() & unlock()대신 synchronized블럭을 사용할 수 있으며, 그럴 때는 그냥
synchromized블럭을 나을 떄가 있다.
ReentrantLock과 Condition
wait(), notify() 로 쓰레드의 종류를 구분하지 않고, 공유 객체의 waiting pool에 같이 몰아 넣는 대신, 다른 쓰레드를 위한 Condition과 또다른 Condition을 만들어서 각각의 waiting pool에서 따로 기다리도록 한다.
이것이 Condition이다.
Codition은 이미 생성된 lock으로 부터 newCondition()을 호출해서 생성한다.
private ReentrantLock lock = new ReetrantLock(); //lock을 생성
private Condition forCook = lock.newCondition();
private Condition forCus = lock.newCondition();
위의 코드에서 두 개의 Condition을 생성했는데, 하나는 요리사 쓰레드를 위한 것이고, 다른 하나는 손님 쓰레드를 위한 것이다.
그 다음엔 wait(), notify() 대신 Condition의 await() signal()을 사용한다.
Object | Condition |
void wait() | void await() vpod awaitUnonterruptibly() |
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() |
지난 예제에서 조금수정해보겠다.
package ch13.Synchronized;
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
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);
}
}
}
class Cook3 implements Runnable{
private Table3 table;
Cook3(Table3 table){this.table = table;}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
int idx =(int)(Math.random()*table.dishNum());
table.add(table.dishNames[idx]);
try {Thread.sleep(10);}catch(InterruptedException e) {}
}
}
}
class Table3 {
String[] dishNames = { "pizza", "pizza", "bread" };
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();
Thread.sleep(500);
}catch(InterruptedException e) {}
}
dishes.add(dish);
forCust.signal();
System.out.println("Dishes : "+dishes.toString());
}finally {
lock.unlock();
}
}
public void remove(String dishName) {
lock.lock();
String name = Thread.currentThread().getName();
try {
while(dishes.size()==0) {
System.out.println(name + " is waiting.");
try {
forCust.await();
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();
return ;
}
}
try {
System.out.println(name +" is waiting.");
forCust.await();
Thread.sleep(500);
}catch(InterruptedException e) {}
}
}
finally {
lock.unlock();
}
}
public int dishNum() {
return dishNames.length;
}
}
public class SynchronizedEx5 {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Table3 table = new Table3();
new Thread(new Cook3(table),"COOK1").start();
new Thread(new Customer3(table, "pizza"),"Cust1").start();
new Thread(new Customer3(table, "bread"),"Cust2").start();
Thread.sleep(2000);
System.exit(0);
}
}
길긴길구나... 그러나 중요한점은 바뀐점만 설명해보겠다.
class Table3 {
String[] dishNames = { "pizza", "pizza", "bread" };
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();
Thread.sleep(500);
}catch(InterruptedException e) {}
}
dishes.add(dish);
forCust.signal();
System.out.println("Dishes : "+dishes.toString());
}finally {
lock.unlock();
}
}
테이블 클래스이다.
여기에 생성자를 이용해서 lock을 생성했다.
그리고 기존에 wait()대신 forCook.await()과 forCust.await()을 사용했다.
이로써 대기와 통지의 대상이 명확히 구분되었다.
계속 interrupt, wait() 해주는것다 조금 더 구분하기 쉬워졌다.

요리사 쓰레드가 통지를 받아야하는 상황에서 손님 쓰레드가 통지를 받는 경우가 없어졌고, 그리고 기아현상이나 경쟁상태가 확실히 개선된 것이다.
응용
손님이 원하는 음식의 종류로 Condition을 더 세분화 하면, 통지를 받고도 원하는 음식이 없어서 다시 기다리는 일이 없도록 할 수 있을 것같다. 그래서 한번 세분화해보았다.
package ch13.Synchronized;
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
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);
}
}
}
class Cook3 implements Runnable{
private Table3 table;
Cook3(Table3 table){this.table = table;}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
int idx =(int)(Math.random()*table.dishNum());
table.add(table.dishNames[idx]);
try {Thread.sleep(10);}catch(InterruptedException e) {}
}
}
}
class Table3 {
String[] dishNames = { "pizza", "pizza", "bread" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition forCook = lock.newCondition();
private Condition forCust1= lock.newCondition();
private Condition forCust2= 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();
Thread.sleep(500);
}catch(InterruptedException e) {}
}
dishes.add(dish);
if(dish=="pizza") {
forCust1.signal();
}else {
forCust2.signal();
}
System.out.println("Dishes : "+dishes.toString());
}finally {
lock.unlock();
}
}
public void remove(String dishName) {
lock.lock();
String name = Thread.currentThread().getName();
try {
while(dishes.size()==0) {
System.out.println(name + " is waiting.");
try {
if(name=="Cust1") {
forCust1.await();
}else {
forCust2.await();
}
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();
return ;
}
}
try {
System.out.println(name +" is waiting.");
if(name=="Cust1") {
forCust1.await();
}else {
forCust2.await();
}
Thread.sleep(500);
}catch(InterruptedException e) {}
}
}
finally {
lock.unlock();
}
}
public int dishNum() {
return dishNames.length;
}
}
public class SynchronizedEx5 {
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
Table3 table = new Table3();
new Thread(new Cook3(table),"COOK1").start();
new Thread(new Customer3(table, "pizza"),"Cust1").start();
new Thread(new Customer3(table, "bread"),"Cust2").start();
Thread.sleep(2000);
System.exit(0);
}
}
기존과 큰차이점은 없지만
Condition을 음식별로 나누었고 if문을 볼려서 해당 lock을 실행할 수 있도록 유도했다.

물련 결과도 별반차이는 없었지만
경쟁상태나 기아현상에 대해 조금이라도 대처를 한점이라고 생각한다.
'JAVA공부 > 2-쓰레드' 카테고리의 다른 글
쓰레드의 동기화(4) (0) | 2021.10.16 |
---|---|
쓰레드의 동기화(2) (0) | 2021.10.15 |
쓰레드의 동기화 (0) | 2021.10.15 |
쓰레드 제어문(4) (0) | 2021.10.14 |
쓰레드 제어문(3) (0) | 2021.10.14 |
댓글