[Java] - Thread(동기화)

백엔드류·2024년 3월 30일

스레드 2편

이전 포스팅에서 스레드에 대한 기초 개념을 알아보았다. 여기서 멀티스레드로 구현을 하다보면, 동기화는 때에 따라 필수적이다. 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문에 이와같은 상황엔 동기화가 필수적이다.
스레드의 동기화를 위해선, 임계영역과 잠금(lock)을 활용한다.
임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념이다. 따라서 임계구역 안에서 수행할 코드가 완료되면, lock을 반납해야한다.



스레드 동기화 방법

  • 임계영역 : 공유 자원에 단 하나의 스레드만 접근하도록(하나의 프로세스에 속한 스레드만 가능)

  • 뮤텍스 : 공유 자원에 단 하나의 스레드만 접근하도록(서로 다른 프로세스에 속한 스레드도 가능)

  • 이벤트 : 특정한 사건 발생을 다른 스레드에게 알림

  • 세마포어 : 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근 제한

  • 대기 가능 타이머 : 특정 시간이 되면 대기 중이던 스레드 깨움



예제코드(동기화x)

아래의 코드는 의도적으로 계좌에 동시 접근이 가능하도록 만든 코드이다. 출금할 금액이 계좌 잔액보다 크면, 출금을 못하도록 설정하였으나, 실행 과정에서 의도적으로 스레드가 동시에 접근할 수 있도록 하였다.

public class ThreadSyncEx {
   public static void main(String[] args) {
       Runnable thread = new CreateThread();
       // 2개의 작업 스레드 생성
       Thread thread1 = new Thread(thread);
       Thread thread2 = new Thread(thread);

       thread1.setName("스레드1");
       thread2.setName("스레드2");

       thread1.start();
       thread2.start();
   }
}

class Money {
   // 현재 가지고 있는 금액
   private int myMoney = 10000;

   public int getMyMoney() {
       return myMoney;
   }

   public boolean withdraw(int money) {
       // 가지고 있는 금액이 출금할 금액보다 크거나 같을 때만 출금
       if (myMoney >= money) {
           // 스레드 동시 접근을 위한 코드
           try {
               Thread.sleep(1000);
           } catch (Exception e) {
               System.out.println(e);
           }

           // 출금
           myMoney -= money;

           return true;
       }
       return false;
   }
}

class CreateThread implements Runnable {
   Money myMoney = new Money();

   public void run() {
       while (myMoney.getMyMoney() > 0) {
           // 1000 ~ 5000원씩 출금
           int money = (int)(Math.random() * 5 + 1) * 1000;

           // 출금 실행 코드. 실패시 true 반환
           boolean denied = !myMoney.withdraw(money);

           // 출금 과정 출력
           if (denied) {
               System.out.println("출금 거부");
           } else {
               System.out.printf("스레드: %s 출금: %d원  남은금액: %d원\\n",
                   Thread.currentThread().getName(), money, myMoney.getMyMoney());
           }
       }
   }
}
// 출력 (실행할 때마다 결과는 달라진다.)
스레드: 스레드1 출금: 5000원  남은금액: 5000원
스레드: 스레드2 출금: 2000원  남은금액: 5000원
스레드: 스레드2 출금: 1000원  남은금액: 4000원
출금 거부
스레드: 스레드1 출금: 1000원  남은금액: 3000원
스레드: 스레드2 출금: 2000원  남은금액: -2000원
스레드: 스레드1 출금: 3000원  남은금액: -2000원

결과를 보면 스레드가 출금을 동시에 실시하여 남은 금액이 3000원임에도 불구하고, 2000원 출금과 3000원 출금이 동시에 발생하여 -2000원되는 상황이 발생했다.
스레드 동기화를 하지 않으면 실제 서비스나 프로그램을 이용할 때, 이러한 상황이 실제로 일어날 수 있다.

임계영역과 lock?

임계 영역은 둘 이상의 스레드가 동시에 접근해서는 안되는 코드 영역이다. 즉, 하나의 스레드만이 코드를 실행할 수 있는 영역이다. lock은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.

🔎 동작과정

  • 먼저, synchronized 키워드를 통해 동시 접근이 가능한 영역을 임계 영역으로 설정하여 동시 접근을 못하도록 설정한다.
  • 스레드가 임계영역에 접근하게 되면, 해당 스레드는 lock을 얻게된다. 이후 해당 스레드가 lock을 반납하기 이전에는 다른 스레드는 해당 임계 영역에 접근하지 못하게 한다.
    -synchronized 키워드는 메서드 전체를 임계 영역으로 설정하는 방법과 특정 코드 블록을 임계 영역으로 설정하는 방법이 있다.


메서드 전체를 임계영역으로 설정

synchronized 키워드를 임계 영역으로 지정할 메서드의 반환 타입 앞에 입력하여 메서드 전체를 임계 영역으로 설정할 수 있다.
설정한 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락(Lock)을 얻으며, 다시 락(Lock)을 반납하기 전까지는 다른 스레드는 해당 메서드를 실행하지 못한다.

class Money {
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    // 메서드 전체를 임계영역으로 설정
    public synchronized boolean withdraw(int money) {
        if (myMoney >= money) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                System.out.println(e);
            }
            myMoney -= money;
            return true;
        }
        return false;
    }
}


특정 영역을 임계 영역으로 설정

임계영역으로 지정할 코드 상단에 synchronized 키워드를 쓰고 소괄호 () 안에 해당 영역이 포함된 객채의 참조를 입력하여 지정할 코드까지 중괄호{}로 묶으면 해당 영역으로 설정된다.

class Money {
    private int myMoney = 10000;

    public int getMyMoney() {
        return myMoney;
    }

    public boolean withdraw(int money) {
        // 메서드 전체를 임계영역으로 설정
        synchronized (this) {
            if (myMoney >= money) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    System.out.println(e);
                }
                myMoney -= money;
                return true;
            }
            return false;
        }
    }
}
// 출력
스레드: 스레드1 출금: 1000원  남은금액: 9000원
스레드: 스레드2 출금: 2000원  남은금액: 7000원
스레드: 스레드1 출금: 4000원  남은금액: 3000원
출금 거부
출금 거부
스레드: 스레드1 출금: 3000원  남은금액: 0원

잔액이 음수가 발생할 경우가 생기더라도 스레드 동기화로 인해 출금이 거부된다!!



출처 : https://ittrue.tistory.com/173

profile
공부한 내용을 정리한 블로그입니다 & 백엔드 개발자

0개의 댓글