멀티 쓰레드의 경우 여러 쓰레드가 한 프로세스의 자원을 공유해서 작업하기 때문에 서로에게 영향을 줄 수 있다. 이로인해 장애나 버그가 발생할 수 있다.
- 이러한 일을 방지하기 위해 한 쓰레드가 진행중인 작업을 다른 쓰레디가 침범하지 못하도록 막는것을 쓰레드 동기화(Synchronization) 라고한다.
- 동기화를 하려면 다른 쓰레드의 침범을 막아야하는 코드들을 '임계영역'으로 설정하면 된다.
- 임계영역에는 Lock을 가진 단 하나의 쓰레드만 출입이 가능하다.
- 즉, 임계영역은 한번에 한 쓰레드만 사용이 가능하다.
public synchronized void asyncSum() {
...침범을 막아야하는 코드...
}
synchronized(해당 객체의 참조변수) {
...침범을 막아야하는 코드...
}
3개의 쓰레드가 1초에 사과 1개씩 먹는 작업
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
if (storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
남은 사과의 수가 뒤죽박죽 출력될뿐만 아니라 사과가 3개 미만으로 남아 있을 때 쓰레드 3개 모두 작업을 시작 해버리면 없는 사과를 먹는 경우도 발생한다.
public class Main {
public static void main(String[] args) {
AppleStore appleStore = new AppleStore();
Runnable task = () -> {
while (appleStore.getStoredApple() > 0) {
appleStore.eatApple();
System.out.println("남은 사과의 수 = " + appleStore.getStoredApple());
}
};
for (int i = 0; i < 3; i++) {
new Thread(task, "thread" + i).start();
}
}
}
class AppleStore {
private int storedApple = 10;
public int getStoredApple() {
return storedApple;
}
public void eatApple() {
synchronized (this) {
if(storedApple > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
storedApple -= 1;
}
}
}
}
침범을 막은 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()을 호출하여 쓰레드가 Lock을 반납하고 기다리게 할 수 있다. (위의 일시정지와 다른 상태)
- 다른 쓰레드가 Lock을 얻어 해당 객체에 대한 작업을 수행할 수 있게 된다.
- 추후에 작업을 진행할 수 있는 상황이 되면 notify()를 호출한다.
- 작업을 중단했던 쓰레드가 다시 Lock을 얻어 진행할 수 있게 된다.
public class Main {
public static String[] itemList = {
"MacBook", "IPhone", "AirPods", "iMac", "Mac mini"
};
public static AppleStore appleStore = new AppleStore();
public static final int MAX_ITEM = 5;
public static void main(String[] args) {
// 가게 점원
Runnable StoreClerk = () -> {
while (true) {
int randomItem = (int) (Math.random() * MAX_ITEM);
appleStore.restock(itemList[randomItem]);
try {
Thread.sleep(50);
} catch (InterruptedException ignored) {
}
}
};
// 고객
Runnable Customer = () -> {
while (true) {
try {
Thread.sleep(77);
} catch (InterruptedException ignored) {
}
int randomItem = (int) (Math.random() * MAX_ITEM);
appleStore.sale(itemList[randomItem]);
System.out.println(Thread.currentThread().getName() + " Purchase Item " + itemList[randomItem]);
}
};
new Thread(StoreClerk, "StoreClerk").start();
new Thread(Customer, "Customer1").start();
new Thread(Customer, "Customer2").start();
}
}
class AppleStore {
private List<String> inventory = new ArrayList<>();
public void restock(String item) {
synchronized (this) {
while (inventory.size() >= Main.MAX_ITEM) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 꽉 차있어서 재입고하지 않고 기다리는 중!
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 재입고
inventory.add(item);
notify(); // 재입고 되었음을 고객에게 알려주기
System.out.println("Inventory 현황: " + inventory.toString());
}
}
public synchronized void sale(String itemName) {
while (inventory.size() == 0) {
System.out.println(Thread.currentThread().getName() + " Waiting!");
try {
wait(); // 재고가 없기 때문에 고객 대기중
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while (true) {
// 고객이 주문한 제품이 있는지 확인
for (int i = 0; i < inventory.size(); i++) {
if (itemName.equals(inventory.get(i))) {
inventory.remove(itemName);
notify(); // 제품 하나 팔렸으니 재입고 하라고 알려주기
return; // 메서드 종료
}
}
// 고객이 찾는 제품이 없을 경우
try {
System.out.println(Thread.currentThread().getName() + " Waiting!");
wait();
Thread.sleep(333);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
Inventory 현황: [IPhone]
Inventory 현황: [IPhone, AirPods]
Customer1 Waiting!
Customer2 Purchase Item AirPods
Customer1 Waiting!
Customer2 Waiting!
Inventory 현황: [IPhone, IPhone]
Customer1 Waiting!
Inventory 현황: [IPhone, IPhone, Mac mini]
Customer2 Waiting!
Inventory 현황: [IPhone, IPhone, Mac mini, IPhone]
Customer1 Purchase Item Mac mini
Customer2 Waiting!
Customer1 Waiting!
Inventory 현황: [IPhone, IPhone, IPhone, AirPods]
Customer2 Waiting!
Inventory 현황: [IPhone, IPhone, IPhone, AirPods, Mac mini]
Customer1 Purchase Item AirPods
Customer2 Waiting!
Customer1 Purchase Item IPhone
Inventory 현황: [IPhone, IPhone, Mac mini, MacBook]
Customer2 Purchase Item MacBook
Customer1 Waiting!
Inventory 현황: [IPhone, IPhone, Mac mini, AirPods]
Customer2 Purchase Item Mac mini
Customer1 Purchase Item AirPods
Inventory 현황: [IPhone, IPhone, Mac mini]
Inventory 현황: [IPhone, IPhone, Mac mini, IPhone]
Customer2 Waiting!
Customer1 Waiting!
Inventory 현황: [IPhone, IPhone, Mac mini, IPhone, Mac mini]
Customer2 Waiting!
StoreClerk Waiting!
종료되지 않고 무한 대기 상황에 걸렸다.
고의적으로 데드락을 걸기위해 짜여진 코드지만 요점은 점원이 사용하는 notify와 고객이 사용하는 notify가 서로를 깨워주지 못하고 있다는 것이다.
synchronized 블러으로 동기화 하면 자동적으로 Lock이 걸리고 풀리지만 같은 메서드 내에서만 Lock을 걸 수 있다는 제약이 있다.
이런 제약을 해결하기 위해 Lock 클래스를 사용한다.
ReentantLock
public class MyClass {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void methodA() {
synchronized (lock1) {
methodB();
}
}
public void methodB() {
synchronized (lock2) {
// do something
methodA();
}
}
}
ReentrantReadWriteLock
StampLock
wait(), notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못하는 것을 해결한 것이 Condition이다.
wait()과 notify()는 객체에 대한 모니터링 락(lock)을 이용하여 스레드를 대기시키고 깨운다. 그러나 wait()과 notify()는 waiting pool 내에 대기중인 스레드를 구분하지 못하므로, 특정 조건을 만족하는 스레드만 깨우기가 어렵다.
이러한 문제를 해결하기 위해 JDK 5에서는 java.util.concurrent.locks 패키지에서 Condition 인터페이스를 제공한다. Condition은 waiting pool 내의 스레드를 분리하여 특정 조건이 만족될 때만 깨우도록 할 수 있으며, ReentrantLock 클래스와 함께 사용된다. 따라서 Condition을 사용하면 wait()과 notify()의 문제점을 보완할 수 있다.
public class Main {
public static final int MAX_TASK = 5;
private ReentrantLock lock = new ReentrantLock();
// lock으로 condition 생성
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private ArrayList<String> tasks = new ArrayList<>();
// 작업 메서드
public void addMethod(String task) {
lock.lock(); // 임계영역 시작
try {
while(tasks.size() >= MAX_TASK) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
condition1.await(); // wait(); condition1 쓰레드를 기다리게 합니다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
tasks.add(task);
condition2.signal(); // notify(); 기다리고 있는 condition2를 깨워줍니다.
System.out.println("Tasks:" + tasks.toString());
} finally {
lock.unlock(); // 임계영역 끝
}
}
}
condition1 = 점원
condition2 = 고객
코드 이외 부분
물건이 없어 condition2는 await 상태
addMethod()를 호출하면서 lock.lock()으로 해당 쓰레드가 addMethod에 들어가서 문을 잠금
코드 부분
물건을 하나 등록하고 고객인 condition2를 지정해서 signal()을 실행시켜 점원 과 고객을 구분할 수 있게 되었음.
모든 작업이 끝나고 lock.unlock()으로 해당 쓰레드가 문 잠금을 해제하고 나감
사실 이 코드도 wait notify 코드처럼 고객의 구매를 랜덤상품으로 정하고 물건을 채우는것도 랜덤상품으로 하면 결국 무한 대기에 빠진다.
요점은 waiting pool에 있는 대기 쓰레드를 지정해서 깨울 수 있다는 것.