*노란색 : Critical Section
#Park 과 ParkWife 가 동시에 Bank 자원에 접근하는 경우의 예제
package synchronization_multithread;
class Bank{ // Shared Resource
private int money = 10000;
public void saveMoney(int save){
int m = this.getMoney();
try {
Thread.sleep(3000); // 3초
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
setMoney( m + save); // money를 저장
}
public synchronized void minusMoney(int minus){
int m = this.getMoney();
try {
Thread.sleep(200); // 0.2초
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
setMoney( m - minus);
}
public int getMoney(){
return money;
}
public void setMoney(int money){
this.money = money;
}
}
class Park extends Thread{
public void run(){
System.out.println("start save");
SyncMain.myBank.saveMoney(3000); //3000원을 save
System.out.println("saveMoney(3000): " + SyncMain.myBank.getMoney() );
}
}
class ParkWife extends Thread{
public void run(){
System.out.println("start minus");
SyncMain.myBank.minusMoney(1000); // 1000원을 소비
System.out.println("minusMoney(1000): " + SyncMain.myBank.getMoney() );
}
}
public class SyncMain {
public static Bank myBank = new Bank(); // Shared Resource
public static void main(String[] args) {
Park p = new Park();
p.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
ParkWife pw = new ParkWife();
pw.start();
}
}
코드 결과 :
기존에 10000원이 있던 은행에서 3000원을 넣고, 1000원을 빼는 연산을 수행했는데, 10000원에서 1000원을 뺀 결과와, 10000원에서 3000원을 더한 결과가 나와서 결국, 은행에는 13000원이 남는 결과가 발생한다.. 즉, 은행이 곧 입출금 단위로 트랜잭션을 임계영역을 설정할 필요가 있다.
Bank (10000원)
Park Park's Wife
3000원 Save
0.2초 Delay...
3초 delay.. 1000원 Minus
결과 : 13000원 결과 : 9000원
Synchronization 블럭과 메서드를 이용해 코드수정 :
class Bank{ // Shared Resource
private int money = 10000;
public void saveMoney(int save){
synchronized (this) {
int m = this.getMoney();
try {
Thread.sleep(3000); // 3초
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
setMoney( m + save); // money를 저장
}
}
public synchronized void minusMoney(int minus){
int m = this.getMoney();
try {
Thread.sleep(200); // 0.2초
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
setMoney( m - minus);
}
수정 결과 :
리소스가 어떤 조건에서 더 이상 유효하지 않은 경우 리소스를 기다리기 위해 Thread 가 wait() 상태가 된다.
wait() 상태가 된 Thread은 notify() 또는 notifyAll()이 호출 될 때까지 기다린다.
유효한 자원이 생기면 notify()가 호출되고, wait() 하고 있는 Thread 중 Random한 하나의 Thread를 재시작 하도록 한다.
(우선순위 또는 wait시간이 긴 쓰레드가 반드시 재시작하는 것이 아님)
notifyAll()이 호출되는 경우 wait() 하고 있는 모든 Thread가 재시작 된다. (이 경우 유효한 리소스만큼의 Thread만이 수행될 수 있고 자원을 갖지 못한 Thread의 경우는 다시 wait() 상태로 만든다)
따라서, 자바에서는 notifyAll() 메서드의 사용을 권장한다.
(Why? 하나의 쓰레드를 깨우는 것 보다 여러 쓰레드를 깨우고 이들에게 유효한 리소스가 생겼을 때, 이들이 그때마다 실행되도록 경쟁(Contention)상태로 놓이는 것이 효율적이고 쓰레드에게도 공평하기 때문)
Ex) 도서관에서 책을 빌리는 상황
여러명이 원하는 책이 반납되었을 때,
한명에게만 알림 = notify() / 대기한 모두에게 알림 = notifyAll()
notify() 예시 :
package librarymultithread;
import java.util.ArrayList;
class FastLibrary{
public ArrayList <String> shelf = new ArrayList();
public FastLibrary() {
shelf.add("태백산맥1");
shelf.add("태백산맥2");
shelf.add("태백산맥3");
}
public synchronized String lendBook() throws InterruptedException { // 책을 빌림
Thread t = Thread.currentThread();
if(shelf.size() == 0) { // 도서관에 책이 없는 경우,
System.out.println(t.getName() + "waiting start");
wait(); // lenBook()메서드를 수행하고 있는 쓰레드를 NonRunnable상태로 바꿔줌
System.out.println(t.getName() + "waiting end");
}
String book = shelf.remove(0);
System.out.println(t.getName() + book + "borrow");
return book;
}
public synchronized void returnBook(String book) { // 책을 반납
Thread t = Thread.currentThread();
shelf.add(book);
notify(); // notify() : object메서드(어느객체에서나 사용가능)
System.out.println(t.getName() + book + "return");
}
}
class Student extends Thread{
public Student(String name) {
super(name);
}
public void run() {
try {
String title = LibraryMain.library.lendBook(); // 책을 빌림
if(title == null) {
System.out.println(getName() + "빌리지 못했음");
return;
}
sleep(5000); // 5초 지연
LibraryMain.library.returnBook(title); // 5초후 책을 반납
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class LibraryMain {
public static FastLibrary library = new FastLibrary();
public static void main(String[] args) {
Student std1 = new Student("std1");
Student std2 = new Student("std2");
Student std3 = new Student("std3");
Student std4 = new Student("std4");
Student std5 = new Student("std5");
std1.start();
std2.start();
std3.start();
std4.start();
std5.start();
}
}
CODE 결과 :
std3와 std2는 책이 없었기 때문에, waiting상태에 있다가, 책이 반납되면서 책을 빌리고 반납까지 완료되는 것을 확인할 수 있다.
위 코드에서는 notify()메서드를 사용했는데, 위 예시에서는 모두 책을 빌리고 반납까지 완료할 수 있었지만, 이론상 notify()에 의해서 특정 쓰레드(학생)가 선택되지 못하면, 해당 학생은 영영 책을 빌리지 못하는 경우가 발생할 수가 있다.
따라서, notifyAll() 메서드를 사용하여 다시 코드를 구현해보았다.
notifyAll()메서드 사용
package librarymultithread;
import java.util.ArrayList;
class FastLibrary{
public ArrayList <String> shelf = new ArrayList();
public FastLibrary() {
shelf.add("태백산맥1");
shelf.add("태백산맥2");
shelf.add("태백산맥3");
}
public synchronized String lendBook() throws InterruptedException { // 책을 빌림
Thread t = Thread.currentThread();
while(shelf.size() == 0) { // 도서관에 책이 없는 경우,
System.out.println(t.getName() + "waiting start");
wait(); // lenBook()메서드를 수행하고 있는 쓰레드를 NonRunnable상태로 바꿔줌
System.out.println(t.getName() + "waiting end");
}
String book = shelf.remove(0);
System.out.println(t.getName() + book + "borrow");
return book;
}
public synchronized void returnBook(String book) { // 책을 반납
Thread t = Thread.currentThread();
shelf.add(book);
notifyAll(); // notify() : object메서드(어느객체에서나 사용가능)
System.out.println(t.getName() + book + "return");
}
}
class Student extends Thread{
public Student(String name) {
super(name);
}
public void run() {
try {
String title = LibraryMain.library.lendBook(); // 책을 빌림
if(title == null) {
System.out.println(getName() + "빌리지 못했음");
return;
}
sleep(5000); // 5초 지연
LibraryMain.library.returnBook(title); // 5초후 책을 반납
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class LibraryMain {
public static FastLibrary library = new FastLibrary();
public static void main(String[] args) {
Student std1 = new Student("std1");
Student std2 = new Student("std2");
Student std3 = new Student("std3");
Student std4 = new Student("std4");
Student std5 = new Student("std5");
Student std6 = new Student("std6");
std1.start();
std2.start();
std3.start();
std4.start();
std5.start();
std6.start();
}
}
CODE 결과 :
book이 반납되어 library에 책이 생겼을 때, wait상태의 모든 쓰레드(학생)를 깨우지만, 여러 쓰레드 중, 하나의 쓰레드만 Runnable()상태로 돌입하게 되고, 나머지 쓰레드는 다시 wait상태로 돌입하게 되는 것을 확인할 수 있었다.