본문 바로가기
Java/Java

쓰레드) 동기화(Synchronization)이란?

by 박채니 2022. 4. 4.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


동기화(Synchronization)란?

- 멀티 쓰레딩 프로그래밍에서 쓰레드 간 공유 자원에 대한 처리

- 공유 자원에 대해 사용 순서를 정해주는 것

- lock(key)을 획득한 쓰레드만 사용 가능

 

쓰레드가 공유 자원에 대한 동기화 미사용 시

두 대의 ATM기를 이용해 출금을 하는 프로그램을 구현해보았습니다.

public class SynchronizationStudy {
	public static void main(String[] args) {
		SynchronizationStudy study = new SynchronizationStudy();
		study.test();
	}
	
	public void test() {
		Account acc = new Account();
		Runnable run1 = new Atm(acc);
		Runnable run2 = new Atm(acc);
		Thread atm1 = new Thread(run1, "ATM1");
		Thread atm2 = new Thread(run2, "ATM2");
		atm1.start();
		atm2.start();
	}
	
	static class Atm implements Runnable {
		private Account acc;
		
		public Atm(Account acc) {
			this.acc = acc;
		}
		
		@Override
		public void run() {
			while(acc.getBalance() > 0) {
				int money = (int)(Math.random()*3 + 1)*100;
				acc.withdraw(money);
				
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 종료!");
		}
	}
	
	static class Account {
		private int balance = 1000;	//잔액
		
		public int getBalance() {
			return balance;
		}
		
		public void withdraw(int money) {
			String thName = Thread.currentThread().getName();
			
			if(balance >= money) {
				balance -= money;
				System.out.printf("[%s] %d원 출금 -> 잔액 : %d원\n", thName, money, balance);
			} else {
				System.out.printf("[%s] %d원 출금시도 -> 잔액이 부족합니다.\n", thName, money);
			}
		}
	}
}

@콘솔출력값
[ATM2] 300원 출금 -> 잔액 : 400원
[ATM1] 300원 출금 -> 잔액 : 400원
[ATM1] 100원 출금 -> 잔액 : 300원
[ATM2] 100원 출금 -> 잔액 : 300원
[ATM2] 200원 출금 -> 잔액 : 200원
[ATM1] 100원 출금 -> 잔액 : 200원
[ATM1] 100원 출금 -> 잔액 : 100원
[ATM2] 200원 출금 -> 잔액 : 100원
[ATM2] 300원 출금시도 -> 잔액이 부족합니다.
[ATM1] 300원 출금시도 -> 잔액이 부족합니다.
[ATM2] 300원 출금시도 -> 잔액이 부족합니다.
[ATM1] 300원 출금시도 -> 잔액이 부족합니다.
[ATM1] 300원 출금시도 -> 잔액이 부족합니다.
[ATM2] 100원 출금 -> 잔액 : 0원
ATM1 종료!
ATM2 종료!

출력값을 확인해보면, 1000원에서 300원을 출금했더니 잔액이 400원이 되고... 잔액이 100원일 때 200원을 출금하려고 하니 출금이 되었고 잔액이 또 100원이 남는 정말 이상한 출금 내역이 리턴 되었습니다.

이는 멀티 쓰레드가 공유 자원(Account)을 같이 사용해서인데요,

ATM2가 300원을 출금하고 있을 때 ATM1이 끼어들어서 300원을 출금하고 잔액 표시를 했기 때문인데요.

 

이처럼 공유 자원에 대해서 멀티 쓰레드의 동기화 사용 (사용 순서)을 하지 않으면 위와 같은 결과를 초래하게 됩니다.

 

동기화 사용 시

동기화 처리 방법 ① synchronized 메소드 작성

- 메소드를 가진 객체(Account)가 격리 공간으로 지정

public class SynchronizationStudy {
	public static void main(String[] args) {
		SynchronizationStudy study = new SynchronizationStudy();
		study.test();
	}
	
	public void test() {
		Account acc = new Account();
		Runnable run1 = new Atm(acc);
		Runnable run2 = new Atm(acc);
		Thread atm1 = new Thread(run1, "ATM1");
		Thread atm2 = new Thread(run2, "ATM2");
		atm1.start();
		atm2.start();
	}
	
	static class Atm implements Runnable {
		private Account acc;
		
		public Atm(Account acc) {
			this.acc = acc;
		}
		
		@Override
		public void run() {
			while(acc.getBalance() > 0) {
				int money = (int)(Math.random()*3 + 1)*100;
				acc.withdraw(money);
				
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 종료!");
		}
	}
	
	static class Account {
		private int balance = 1000;	//잔액
		
		public int getBalance() {
			return balance;
		}
		
		public synchronized void withdraw(int money) {
			System.out.println("+++++++++++++++++++++++++++");
			String thName = Thread.currentThread().getName();
			
			if(balance >= money) {
				balance -= money;
				System.out.printf("[%s] %d원 출금 -> 잔액 : %d원\n", thName, money, balance);
			} else {
				System.out.printf("[%s] %d원 출금시도 -> 잔액이 부족합니다.\n", thName, money);
			}
			System.out.println("---------------------------");
		}
	}
}

@콘솔출력값
+++++++++++++++++++++++++++
[ATM1] 100원 출금 -> 잔액 : 900원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 100원 출금 -> 잔액 : 800원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 300원 출금 -> 잔액 : 500원
---------------------------
+++++++++++++++++++++++++++
[ATM1] 300원 출금 -> 잔액 : 200원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 200원 출금 -> 잔액 : 0원
---------------------------
+++++++++++++++++++++++++++
[ATM1] 100원 출금시도 -> 잔액이 부족합니다.
---------------------------
ATM1 종료!
ATM2 종료!

동기화를 사용하니, ATM1이 공유자원(Account)을 사용하는 동안 ATM2는 사용할 수 없어서 기다리고 있다가 ATM1이 끝나면 다시 그 공유자원을 사용하기 위해 ATM1과 ATM2가 lock을 잡아 사용하고, lock을 잡지 못한 쓰레드는 기다리게 됩니다.

따라서 위와 같이 출금 금액에 따른 잔액이 올바르게 리턴되는 것이죠.

 

② 메소드 내 synchronized 블록 작성

- 지정한 객체가 격리 공간으로 지정

public class SynchronizationStudy {
	public static void main(String[] args) {
		SynchronizationStudy study = new SynchronizationStudy();
		study.test();
	}
	
	public void test() {
		Account acc = new Account();
		Runnable run1 = new Atm(acc);
		Runnable run2 = new Atm(acc);
		Thread atm1 = new Thread(run1, "ATM1");
		Thread atm2 = new Thread(run2, "ATM2");
		atm1.start();
		atm2.start();
	}
	
	static class Atm implements Runnable {
		private Account acc;
		
		public Atm(Account acc) {
			this.acc = acc;
		}
		
		@Override
		public void run() {
			while(acc.getBalance() > 0) {
				int money = (int)(Math.random()*3 + 1)*100;
				acc.withdraw(money);
				
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(Thread.currentThread().getName() + " 종료!");
		}
	}
	
	static class Account {
		private int balance = 1000;	//잔액
		
		public int getBalance() {
			return balance;
		}
		
		public void withdraw(int money) {
			synchronized(this) {
				System.out.println("+++++++++++++++++++++++++++");
				String thName = Thread.currentThread().getName();
				
				if(balance >= money) {
					balance -= money;
					System.out.printf("[%s] %d원 출금 -> 잔액 : %d원\n", thName, money, balance);
				} else {
					System.out.printf("[%s] %d원 출금시도 -> 잔액이 부족합니다.\n", thName, money);
				}
				System.out.println("---------------------------");
			}
		}
	}
}

@콘솔출력값
+++++++++++++++++++++++++++
[ATM1] 100원 출금 -> 잔액 : 900원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 100원 출금 -> 잔액 : 800원
---------------------------
+++++++++++++++++++++++++++
[ATM1] 300원 출금 -> 잔액 : 500원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 200원 출금 -> 잔액 : 300원
---------------------------
+++++++++++++++++++++++++++
[ATM1] 300원 출금 -> 잔액 : 0원
---------------------------
+++++++++++++++++++++++++++
[ATM2] 100원 출금시도 -> 잔액이 부족합니다.
---------------------------
ATM2 종료!
ATM1 종료!

마찬가지로 (this)의 lock을 잡은 ATM1 쓰레드와 ATM2 쓰레드가 사용하는 중에는 (this) lock을 잡지 못한 쓰레드는 기다리게 되고, 쓰레드가 사용을 마치면 다시 ATM1 쓰레드와 ATM2 쓰레드 중 lock을 잡은 쓰레드가 실행되게 됩니다.