본문 바로가기

자바

synchronized

멀티스레드를 잘 사용하면 프로그램적으로 좋은 성능을 낼 수 있지만, 멀티스레드 환경에서 반드시 고려해야할 점인 스레드 간 동기화라는 문제는 꼭 해결해야 한다.

예를 들어 스레드 간 서로 공유하고 수정할 수 있는 data가 있는데 스레드간 동기화가 되지 않은 상태에서 멀티스레드 프로그램을 돌리면, data의 안정성과 신뢰성을 보장할 수 없다.

따라서 data의 thread-safe를 하기 위해 자바에서는 synchronized 키워드를 제공해 스레드간 동기화를 시켜 data의 thread-safe를 가능케한다.

자바에서 지원하는 synchronized 키워드는 여러 개의 스레드가 한 개의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근할 수 없도록 막는 개념이다.

synchronized 키워드는 변수와 함수에 사용해서 동기화 할 수 있다. 하지만 synchronized 키워드를 너무 남발하면 오히려 프로그램 성능 저하를 일으킬 수 있다.

그 이유는 synchronized 키워드를 사용하면 자바 내부적으로 메소드나 변수에 동기화를 하기 위해 block과 unblock 을 처리하게 되는데 이런 처리들이 만약 너무 많아지게 되면 오히려 프로그램 성능저하를 일으킬 수 있는 것이다.

(block과 unblock도 프로그램 내부적으로 어느 정도 공수가 들어가는 작업이다.)

따라서 적재적소에 synchronized 키워드를 사용하는 것이 중요하다.

1. 메소드에서 사용하는 경우

public synchronized void method() {}

 

2. 객체 변수에 사용하는 경우(block문)

priavte Object obj = new Object();

public void exampleMethod() { synchronized(obj) } {// 코드 }}

 

package thread;

public class ThreadSynchronizedTest {

	public static void main(String[] args) {
		
		Task task = new Task();
		
		Thread t1 = new Thread(task);
		Thread t2 = new Thread(task);
		
		t1.setName("t1-Thread");
        t2.setName("t2-Thread");
         
        t1.start();
        t2.start();
	}
}

class Account {
	int balance = 1000;
	
	public void withDraw(int money) {
		if(balance >= money) {
			try {
				Thread thread = Thread.currentThread();
				System.out.println(thread.getName() + " 출금 금액 ->> "+money);
				Thread.sleep(1000);
                balance -= money;
                System.out.println(thread.getName()+" balance:" + balance);
                
			}catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

class Task implements Runnable {

	Account acc = new Account();
	
	@Override
	public void run() {
		while(acc.balance > 0) {
			// 100, 200, 300 중의 한 값을 임의로 선택해서 출금(withDraw)한다.
			int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withDraw(money);
		}
	}
	
}

 

Account라는 클래스에는 balance 잔액과 이 잔액을 삭감시키는 인출 메소드가 있다.

지난시간 스레드를 만들 때 Runnable 인터페이스를 구현하여 Task를 만들고 이 Task를 Thread 생성시 생성자에게 매개 변수로 넣으면 우리가 만든 스레드가 Task에 정의되어 있는 대로 일을 실행한다고 했습니다.

Runnable을 구현하여 만든 Task에 100, 200, 300 중 랜덤하게 값을 전달받아 money 변수에 할당하고 그 금액만큼 Account 인스턴스의 인출메소드를 호출해 balance(잔액)를 출금시키는 일을 구현해놨습니다.

 

다음 main 메소드에서 스레드 t1, t2를 만들고 각각의 스레드 이름을 정의한다.

t1, t2 스레드가 시작하면 잔액이 0이 될 때까지 두 스레드가 경쟁하며 출금시킬 것이다.

 

결과:

t1-Thread 출금 금액 ->> 100
t2-Thread 출금 금액 ->> 200
t1-Thread balance:900
t2-Thread balance:900
t1-Thread 출금 금액 ->> 300
t2-Thread 출금 금액 ->> 200
t2-Thread balance:400
t1-Thread balance:400
t1-Thread 출금 금액 ->> 100
t2-Thread 출금 금액 ->> 200
t1-Thread balance:100
t1-Thread 출금 금액 ->> 100
t2-Thread balance:100
t2-Thread 출금 금액 ->> 100
t1-Thread balance:-100
t2-Thread balance:-100

 

결과를 보면 분명 잔액이 0이 될 때까지 출금을 하라고 했는데 잔액이 마이너스가 됬다.

여기서 멀티스레드의 문제점이 발견된다. balance(잔액)가 thread-safe가 되지 않았기 때문에 t1 스레드가 잔액을 삭감하기 전에 t2가 balance(잔액)에 접근해 삭감을 해버리고 다시 t1이 sleep()에서 깨어나 balance(잔액)를 삭감해버리기 때문에 잔액이 0 이하의 마이너스 값을 가지게 된다.

 

해결방법

공유데이터에 대한 접근과 수정이 이루어지는 메소드에 synchronized 키워드를 리턴타입 앞에 붙여주면된다.

t1 스레드가 먼저 공유데이터나 메소드에 점유하고 있는 상태인 경우 block으로 처리하기 때문에 t1 이외의 스레드의 접근을 막는다.

t1 스레드가 작업을 다 끝냇다면 .unblock으로 처리하여 t1 이외의 스레드의 접근을 허락한다.

 

 

synchronized 키워드로 멀티스레드 동기화 처리

package thread;

public class ThreadSynchronizedTest {

	public static void main(String[] args) {
		
		Task task = new Task();
		
		Thread t1 = new Thread(task);
		Thread t2 = new Thread(task);
		
		t1.setName("t1-Thread");
        t2.setName("t2-Thread");
         
        t1.start();
        t2.start();
	}
}

class Account {
	int balance = 1000;
	
	public synchronized void withDraw(int money) {
		if(balance >= money) {
			try {
				Thread thread = Thread.currentThread();
				System.out.println(thread.getName() + " 출금 금액 ->> "+money);
				Thread.sleep(1000);
                balance -= money;
                System.out.println(thread.getName()+" balance:" + balance);
                
			}catch (Exception e) {
				// TODO: handle exception
			}
		}
	}
}

class Task implements Runnable {

	Account acc = new Account();
	
	@Override
	public void run() {
		while(acc.balance > 0) {
			// 100, 200, 300 중의 한 값을 임의로 선택해서 출금(withDraw)한다.
			int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withDraw(money);
		}
	}
	
}

synchronized 키워드를 사용함으로써 balance 공유데이터에 대한 thread-safe를 시켰기 때문에 데이터나 메소드 점유하고 있는 스레드가 온전히 자신의 작업을 마칠 수 있다.

 

출처:

coding-start.tistory.com//68

'자바' 카테고리의 다른 글

reflection  (0) 2020.11.11
정규식  (0) 2020.11.11
람다  (0) 2020.11.09
java의 .properties 파일 읽기  (0) 2020.11.09
new 연산자  (0) 2020.11.09