멀티 Thread프로그래밍에서의 동기화 개념 및 구현
Critical Section(임계영역)과 Semaphore
- critical section은 두 개 이상의 thread가 동시에 접근할 수 없는 영역
- semaphore 는 특별한 형태의 시스템 객체이며 get/release 두 개의 기능이 있음
- 한순간에 오직 하나의 Thread만이 Semaphore를 얻을 수 있고, 나머지 Thread들은 대기상태(일종의 경쟁상태)가 됨
- Semaphore를 얻은 Thread만이 Critical Section에 들어갈 수 있으며, 해당 Thread가 Shared Resource를 이용해 작업을 진행
*노란색 : 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);
}
수정 결과 :
동기화 (synchronization)
- 두 개의 thread 가 같은 객체에 접근 할 경우, 동시에 접근 함으로써 오류가 발생
- 동기화는 임계영역에 접근한 경우 공유자원을 lock 하여 다른 thread의 접근을 제어
*Java에서는 메서드에 synchronized블럭 또는 synchronized 메서드 로 명시하면, 동기화를 할 수 있게 된다. 즉, 해당 메서드가 포함된 객체 또는 인스턴스를 lock시킬 수 있다.
- 동기화를 잘못 구현하면 deadlock(교착상태)에 빠질 수 있다.
*자바에서는 deadlock을 방지하는 기술이 제공되지 않으므로 되도록이면 synchronized 메서드에서 다른 synchronized 메서드는 호출하지 않도록 한다.
<DeadLock의 예>
wait()/notify() 메서드를 활용한 동기화
-
리소스가 어떤 조건에서 더 이상 유효하지 않은 경우 리소스를 기다리기 위해 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상태로 돌입하게 되는 것을 확인할 수 있었다.
Author And Source
이 문제에 관하여(멀티 Thread프로그래밍에서의 동기화 개념 및 구현), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@soind963/멀티-Thread프로그래밍에서의-동기화-개념-및-구현저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)