멀티 Thread프로그래밍에서의 동기화 개념 및 구현

Jerry Kim·2021년 8월 15일
0

CS_STUDY

목록 보기
5/5

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상태로 돌입하게 되는 것을 확인할 수 있었다.

profile
Welcome to Jerry's World

0개의 댓글