스레드 동기화(Thread Synchronized)

김운채·2023년 5월 9일
0

TIL

목록 보기
4/22

스레드 동기화

자바에서 멀티스레드를 이용하면 여러작업을 동시에 처리할 수 있기 때문에 작업효율이 좋아질 수도 있다.

하지만 하나의 공유자원을 여러 스레드에서 동시에 접근하여 사용하게 되면 때때로 우리가 예상치 못한 결과를 마주할 수 있다.

쓰레드가 동시성으로 실행될 때 여러 쓰레드가 동시에 접근 가능한 자원을 공유자원이라고 한다.

어떤 문제가 생길 수 있는지 예제를 보면서 이해해보자.


잔고가 1000원이 있다.
잔고에서 랜덤한 금액을 출금하는 메서드를 호출하여 출금한다. 이때 잔고에 남은 금액이 출금하려는 돈보다 크거나 같아야 출금할 수 있어야 한다.

class Account{
	private int balance = 1000;
    
    public int getBalance(){
    	return balance;	
    }
    
    public void withdraw(int money){
    	if(balance >= money){ // 잔고의 금액이 출금하려는 돈보다 크거나 같아야
        	try{Thread.sleep(1000)
            }catch(InterruptedException e){}
            balance -= money;
        }
    }
}

class RunnableEx implements Runnable{
	Account acc = new Account();
    
    public void run(){
    	while(acc.getBalance()>0){
        	int money = (int)(Math.random()+3+1)+100;
            acc.withdraw(money);
            System.out.println(acc.getBalance());
        }
    }
}

class ThreadEx {
	public static void main(String args[]){
    	RunnableEx r = new RunnableEx();
        new Thread(r).start();
        new Thread(r).start();
    }
}
900
600
500
200
-100

음수가 나오면 안되는 로직인데 음수가 나왔다.
어떻게 된 것일까?
프로세스는 이랬을 것이다.

  1. 스레드 A가 잔고에서 200원을 빼려고 한다. (현재잔고 200원)
  2. 스레드 A가 if(balance >= money) 를 통과했다. (현재잔고 200원)
  3. 근데 이때 스레드 B도 100원을 빼려고 한다. 스레드 B가 if(balance >= money) 를 통과했다. (현재 잔고 200원)
  4. 스레드 B가 100원을 뺀다. (현재 잔고 100원)
  5. 스레드 A가 200원을 뺀다.
  6. 현재 잔고 -100원

이런 눈물나는 오류가 생길 수 있기 때문에 한 스레드가 진행중인 작업을 다른 스레드가 간섭하지 못하게 막아야 한다.

그래서 스레드 동기화란,
👉 멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는것

syncronized

앞서 말한 문제를 syncronized 를 사용해서 스레드를 동기화 할 수 있다.
동기화하려면 간섭받지 않아야하는 문장들을 임계영역(critical section)으로 설정한다.

⭐ synchronized로 지정된 임계영역은 한 스레드가 이 영역에 접근하여 사용할때 lock이 걸림으로써 다른 스레드가 접근할 수 없게 된다.

⭐ 임계영역은 lock 을 얻은 단하나의 스레드만 출입이 가능하다. (1객체 1락)

이후 해당 스레드가 이 임계영역의 코드를 다 실행 후 벗어나게되면 unlock 상태가 되어 그때서야 대기하고 있던 다른 스레드가 이 임계영역에 접근하여 다시 lock을 걸고 사용할 수 있게 된다.

synchronized 로 임계영역을 설정하는 방법은 두가지가 있다.

1) 메소드에 synchronized 설정하기

메소드 이름 앞에 synchronized 키워드를 사용하면 해당 메소드 전체를 임계영역으로 설정한다.

public synchronized void increase() {
	count++;
	System.out.println(count);
}

임계영역은 한 스레드만 출입할 수 있기 때문에 영역을 최소화 해야한다.
=> 임계영역이 많을 수록 성능이 떨어짐, 갯수와 영역도 최소화 하는게 좋음

2) 코드블럭에 synchronized 설정하기

꼭 필요한 부분에만 블럭을 지정하여 임계영역으로 설정할 수 있다.
예제와 같이 synchronized(this)로 지정하게 되면 참조변수(this) 객체의 lock을 사용하게 된다.

void increase() {
	synchronized(this) {//객체의 참조변수
		count++;
	}
	System.out.println(count);
}

그럼 위 잔고 예제에 syncronized 를 걸어보자.

class Account{
	private int balance = 1000; // 동기화해놨어도 다른 스레드가 건드리면 의미가 없게 되니 private으로 선언
    //-----임계영역-----//
    public syncronized int getBalance(){ //잔고를 읽을 때에도 방해받지 않게 동기화를 걸어줬다
    	return balance;	
    }
    
    //-----임계영역-----//
    public syncronized void withdraw(int money){
    	if(balance >= money){
        	try{Thread.sleep(1000)
            }catch(InterruptedException e){}
            balance -= money;
        }
    }
}

class RunnableEx implements Runnable{
	Account acc = new Account();
    
    public void run(){
    	while(acc.getBalance()>0){
        	int money = (int)(Math.random()+3+1)+100;
            acc.withdraw(money);
            System.out.println(acc.getBalance());
        }
    }
}

class ThreadEx {
	public static void main(String args[]){
    	RunnableEx r = new RunnableEx();
        new Thread(r).start();
        new Thread(r).start();
    }
}
900
300
200
0
0

내 잔고를 안전하게 지킬 수 있게 되었다!

0개의 댓글