Java & Kotlin

[Java 기능] 동기화 구현 (2) - 한정적인 리소스에 접근하는 경우

Sue 2022. 2. 19. 16:14

wait(), notify() 메서드를 활용한 동기화 프로그래밍

  • 리소스가 available하지 않은 경우 쓰레드에 wait() 메서드를 호출하여 Not Runnable 상태가 됨
  • wait() 상태가 된 쓰레드는 notify() 메서드가 호출될 때까지 대기함
  • 리소스가 available 해지면 notify() 메서드가 호출되고 wait() 상태에 있는 쓰레드 중 무작위로 하나의 thread를 재시작
  • notifyAll() 메서드가 호출되면 wait() 상태의 모든 쓰레드가 재시작되고 available한 자원만큼의 쓰레드만 수행됨
  • 자원을 갖지못한 쓰레드는 다시 wait() 상태가 됨
  • notify()를 호출하는 경우 무작위로 호출하기 때문에 계속 실행되지 못하는 쓰레드가 있을 수 있음
  • 자바에서는 notifyAll() 메서드의 사용을 권장

 

예제 코드

도서관에서 책을 빌리는 경우 책의 개수보다 학생의 수가 많을 때(리소스가 한정적일 때) 발생할 수 있는 문제 확인

 

학생보다 책 개수가 많으면 문제 x

but 책이 학생보다 적으면 에러뜸

synchronized로 해결불가 (why?)

 

책의 개수와 학생의 수가 같을 때
package ch23;

import java.util.ArrayList;

class Library {	// 도서관
	
	public ArrayList<String> shelf = new ArrayList<>();	// 책이 저장될 list
	
	public Library() {
		shelf.add("태백산맥1");
		shelf.add("태백산맥2");
		shelf.add("태백산맥3");
		shelf.add("태백산맥4");
		shelf.add("태백산맥5");
		shelf.add("태백산맥6");
	}
	
	public String lendBook() {	// 책을 빌리는 메서드
		
		Thread t = Thread.currentThread();	// 메서드를 실행중인 쓰레드
		String book = shelf.remove(0);
		System.out.println(t.getName() + " lend " + book);
		return book;
	}
	
	public void returnBook(String book) {	// 책을 반납하는 메서드
		
		Thread t = Thread.currentThread();
		shelf.add(book);
		System.out.println(t.getName() + " return " + book);
				
	}
	
}

class Student extends Thread {
	
	public Student(String name) {
		super(name);
	}
	
	public void run() {
		
		String title = LibraryMain.lib.lendBook();
		if(title == null) return;
		
		try {
			sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		LibraryMain.lib.returnBook(title);
	}
}

public class LibraryMain {
	
	public static Library lib = new Library();
	
	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();
	}

}

 

  • 자원이 충분히 존재하므로 자원 경쟁이 발생하지 않음
std3 lend 태백산맥3
std2 lend 태백산맥2
std1 lend 태백산맥1
std5 lend 태백산맥4
std6 lend 태백산맥5
std4 lend 태백산맥6
std3 return 태백산맥3
std2 return 태백산맥2
std5 return 태백산맥4
std1 return 태백산맥1
std4 return 태백산맥6
std6 return 태백산맥5

 

책의 개수가 학생 수보다 적을때
  • 메서드를 synchronized로 선언하여 동시에 수행될 수 없도록 함
  • lendBook() : 리소스가 존재하지 않을 때 조건문에 wait() 메서드를 추가함
  • returnBook() : 리소스가 추가되면 notify() 메서드 실행
package ch23;

import java.util.ArrayList;

class Library {
	
	public ArrayList<String> shelf = new ArrayList<>();
	
	public Library() {
		shelf.add("태백산맥1");
		shelf.add("태백산맥2");
		shelf.add("태백산맥3");
//		shelf.add("태백산맥4");
//		shelf.add("태백산맥5");
//		shelf.add("태백산맥6");
	}
	
	public synchronized String lendBook() throws InterruptedException {
		
		Thread t = Thread.currentThread();	// 메서드를 실행중인 쓰레드
		
		if(shelf.size() == 0) {	// 리소스가 없을 경우
			System.out.println(t.getName() + " waiting start");
			wait();	// Object의 메서드
			System.out.println(t.getName() + " waiting end");
		}

		String book = shelf.remove(0);
			System.out.println(t.getName() + " lend " + book);
			return book;
	}
	
	public synchronized void returnBook(String book) {
		
		Thread t = Thread.currentThread();
		shelf.add(book);
		notify();
		System.out.println(t.getName() + " return " + book);
				
	}
	
}

class Student extends Thread {
	
	public Student(String name) {
		super(name);
	}
	
	public void run() {
		try {
			
		String title = LibraryMain.lib.lendBook();
		
		if(title == null) {
			System.out.println(getName() + " 빌리지 못함");
			return;
		}
		
		sleep(5000);
		LibraryMain.lib.returnBook(title);
		
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public class LibraryMain {
	
	public static Library lib = new Library();
	
	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();
	}

}

 

수행 결과
  • notify() 메서드로 하나씩 쓰레드를 무작위로 깨움
std1 lend 태백산맥1
std6 lend 태백산맥2
std5 lend 태백산맥3
std4 waiting start
std3 waiting start
std2 waiting start
std1 return 태백산맥1
std4 waiting end
std4 lend 태백산맥1
std5 return 태백산맥3
std3 waiting end
std3 lend 태백산맥3
std6 return 태백산맥2
std2 waiting end
std2 lend 태백산맥2
std4 return 태백산맥1
std2 return 태백산맥2
std3 return 태백산맥3

 

  • notify() 대신 notifyAll()을 사용할 경우 빌리지 못하는 경우 발생함
  • 자원이 없는 동안 생길 때까지 wait() 하도록 해야함 
std1 lend 태백산맥1
std5 lend 태백산맥2
std2 lend 태백산맥3
std4 waiting start
std3 waiting start
std6 waiting start
std1 return 태백산맥1
std4 waiting end
std4 lend 태백산맥1
std5 return 태백산맥2
std6 waiting end
std6 lend 태백산맥2
std3 waiting end
std3 빌리지 못함
std2 return 태백산맥3
std6 return 태백산맥2
std4 return 태백산맥1

 

wait() 메서드가 적용되는 부분을 while문으로 수정

 

while(shelf.size() == 0) {
    System.out.println(t.getName() + " wait start");
    wait();
    System.out.println(t.getName() + " wait end");
}

 

수행 결과
  • notifyAll()을 수행하면 모든 쓰레드가 깨어남
  • 이때 자원을 가지지 못한 쓰레드는 다시 wait 상태로 돌아감
  • notify()를 수행하는 것보다 공정한 수행 방법
std1 lend 태백산맥1
std6 lend 태백산맥2
std5 lend 태백산맥3
std3 waiting start
std4 waiting start
std2 waiting start
std1 return 태백산맥1
std3 waiting end
std3 lend 태백산맥1
std6 return 태백산맥2
std2 waiting end
std2 lend 태백산맥2
std4 waiting end
std4 waiting start
std5 return 태백산맥3
std4 waiting end
std4 lend 태백산맥3
std3 return 태백산맥1
std2 return 태백산맥2
std4 return 태백산맥3