자바에서의 Thread Safety
Concurrency Programing 이란 여러개의 작업을 동시에 처리하는 프로그래밍 방식입니다.
실제로 우리가 사용하는 대부분의 서비스들은 다중 프로세스 및 스레드를 사용해 동시에 여러 작업을 수행합니다.
하지만 여러 작업들이 동시성을 갖고진행되기 때문에 프로그램은 복잡해지며
다음과 같은 문제들을 야기할 수 있습니다.
- Race Condition 으로 인한 결과값 변동
- Deadlock , Concurrency Bugs 같은 버그
- Synchronized Overhead 같은 성능 문제
이 글에서는 위와같은 문제들을 해결하기 위한 Thread Safety를 집중적으로 다룰 것 입니다.
1. Thread Safety의 필요성
Thread Safety는 왜 필요할까요?
또 똑바로 지켜지지 않는다면 어떤 문제가 발생할 까요?
다음의 코드를 통해서 확인해보겠습니다.
public class Main {
public static void main(String[] args) {
Number number = new Number();
Counter counter1 = new Counter(number, true);
Counter counter2 = new Counter(number, false);
counter1.start();
counter2.start();
while (counter1.isAlive() || counter2.isAlive()) {
} // Thread종료 될떄 까지 기다리기
System.out.println(counter1.getState() + " " + counter2.getState());
System.out.println(number.count);
}
}
class Number {
int count;
public Number() {
this.count = 0;
}
public void increase() {
this.count++;
}
public void decrease() {
this.count--;
}
}
class Counter extends Thread {
private final Number number;
private boolean flag;
public Counter(Number number, boolean flag) {
this.number = number;
this.flag = flag;
}
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
if (flag) {
number.increase();
} else {
number.decrease();
}
}
}
}
Counter를 통해서 Number 객체의 count를 증감시키는 코드입니다.
똑같은 Number 객체를 갖고있기 때문에 Number는 공유자원이 됩니다.
똑같은 횟수를 증감하기 떄문에 출력은 0이 되야 정상이지만 0이 아닌값이 출력됩니다.
왜 이런 일이 생기는 것일까요?
Multi Thread환경에서는 특정 자원(Shared Resources)을 공유하게 됩니다.
위의 코드에서는 Number가 공유 자원에 속합니다.
count 값이 5일때 동시에 increase , decrease 연산이 일어나는 경우를 생각해 보겠습니다.
- incrase,decrease 둘 다 5의 count 값 가짐
- increase 동작 전에 decrease 동작 완료 count 값 4 변경
- increase는 이미 count 값 5를 가지고 있음으로 6으로 증가
- 각각 한번의 increase , decrease 가 일어 났지만 결과 값은 5가 아닌 6
따라서 공유자원에 대한 접근을 제한하지 않는다면 , 즉 Thread Safety하게 만들지 않으면
예상치 못한 결과값이 만들어 질 수 있습니다. (Data Consistency 위반)
좀더 General한 상황에 대해 정리하면 특정 작업을 수행함에 있어 순서를 정해주지 않는다면 문제가 발생할 수 있는 경우에
Thread Safety 를 신경써줘야 합니다.
또 공유 자원에 대한 접근이 이뤄지는 영역을 Critical Section(임계영역) 이라 부릅니다.
2. Thread Safety를 위한 방법
자바에서는 Thread Safety를 지키기 위해
크게 3가지 방법을 사용합니다.
1. Monitor
Synchronized keyword 사용
2. Mutex
Lock 인터페이스를 구현한 클래스 사용 (Lock 기반)
3. Semaphore
Binary Semaphroe , Counting Semaphroe
Semaphore 클래스 사용 (Signal 기반)
각각 무슨 차이가 존재할까요?
이제부터 알아보겠습니다.
3. Monitor
모니터는 모니터 큐를 통해 공유 자원에 대한 작업들을 순차적으로 처리합니다.
만약 공유자원 A에 대해 다수의 Thread가 접근한다면 큐를 이용해 대기열에 집어넣은 후
맨 앞의 스레드를 꺼내 공유자원에 대한 접근을 허용하는 방식입니다.
스레드의 작업이 끝난다면 대기열에 쌓여있는 스레드의 순서대로 순차적으로 작업이 일어납니다.
하나의 스레드만 공유자원에 접근 가능하기 때문에 상호 배제가 자동이며 구현또한 간단합니다.
뒤에서 말할 Mutex , Semaphore보다 저수준의 추상화를 나타냅니다.
자바에서는 Synchronized Keyword를 통해 사용됩니다.
Synchronized
public synchronized void increase() { // number에 대한 모니터락 획득
this.count++;
}
public synchronized void decrease() { //number에 대한 모니터락 획득
this.count--;
}
위에서 사용된 코드지만 메서드에 Synchronized Keyword가 붙었습니다.
Synchronized가 붙음으로써 이 메서드를 호출시 객체 자체에 대해서 Monitor Lock이 걸립니다.
이렇게 메서드를 수정하고 난 후에는 count값이 정상적으로 0 이 나오는 것을 볼 수 있습니다.
메서드에 적용하는 방법 뿐 아니라 , 블록에도 적용 가능합니다.
@Override
public void run() {
for (int i = 0; i < 10000000; i++) {
if (flag) {
synchronized (number) { // number 모니터 락 획득
number.increase();
}
} else {
synchronized (number) { // number 모니터 락 획득
number.decrease();
}
}
}
}
이렇게 사용한다면 객체 자체에 Monitor Lock이 걸리는 것이 아니라 Synchronized로 선언된 블럭에서만
Monitor Lock을 획득해 접근을 제어하는것이 됩니다.
같은 Synchronized Keyword를 사용해도 메서드에 사용하는 것과 블럭에 사용하는것의 차이가 큼으로 유의해서 사용바랍니다.
4. Mutual Exclusion (Mutex)
Mutual Exclusion 은 둘 이상의 thread가 동시에 Critical Section에 접근하는 것을 막음을 의미합니다.
즉 공유자원에 대해서 한번에 하나의 thread만 접근하도록 만드는 것입니다.
일상생활의 예시를 들자면 변기가 하나인 화장실을 생각하면 됩니다.
변기(공유자원)에 대해서 한번에 한 사람만 사용이 가능합니다.
또 사용자는 Lock을 검으로써 (화장실문 잠금) 사용중임을 나타냅니다.
화장실을 전부 사용한 후에는 Unlcok 함으로써 아무나 사용가능한 상태로 변경합니다.
대표적으로 Lock Interface를 구현한 ReentrantLock가 존재합니다.
Lock
Lock Interface는 java.util.concurrent.locks.Lock 에 존재하며
동기화 문제를 해결하기 위한 메커니즘 중 하나입니다. 이를 사용하여 코드 블록을 동기화하고 잠금을 걸 수 있습니다.
주로 Lock 인터페이스를 구현한 클래스 중 하나인 ReentrantLock이 사용됩니다.
Lock lock = new ReentrantLock();
public void increase() {
lock.lock(); // lock 획득
this.count++; // 보호받음
lock.unlock(); // lock 해제
}
public void decrease() {
lock.lock(); // lock 획득
this.count--; // 보호받음
lock.unlock(); // lock 해제
}
여기서 사용된 lock() , unlock() 메서드 외에도 tryLock() , lockInterruptibly() , Condition을 통해서
synchronized 보다 더 유연하게 사용 가능합니다.
5. Semaphore
Semaphore는 일반화된 Mutex라고 생각하면 됩니다.
Mutex는 변기가 하나인 경우를 의미하고 , Semaphore는 변기가 1개 이상인 경우입니다.
java.util.concurrent.Semaphore 에 존재하며 , acquire() , release() 메서드를 통해 사용됩니다.
acquire() 라는 신호를 줌으로써 변기(공유자원)에 대한 사용을 알리고
사용이 끝난경우 release() 라는 신호를 주어 사용이 끝났음을 알립니다.
다음과 같이 사용될 수 있습니다.
import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) {
Number number = new Number();
Counter[] counters = new Counter[5];
for (int i = 0; i < 5; i++) {
counters[i] = new Counter(number);
counters[i].start(); // 5개의 스래드가 돌아가기 때문에 원래는 5씩 증가해야 한다.
}
while (counters[0].isAlive() || counters[1].isAlive() || counters[2].isAlive() || counters[3].isAlive() ||
counters[4].isAlive()) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
System.out.println(number.getCount());
}
System.out.println("종료");
}
}
class Number {
private Semaphore semaphore = new Semaphore(3); // 스래드 3개까지 동시 엑세스 허용
public int getCount() {
return count;
}
private int count;
public Number() {
this.count = 0;
}
public void increase() {
try {
semaphore.acquire(); // 스래드 엑세스 허용
System.out.println("증가 시작");
this.count++;
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
semaphore.release(); // 스래드 엑세스 해제
}
}
class Counter extends Thread {
private final Number number;
public Counter(Number number) {
this.number = number;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
number.increase();
}
}
}
원래라면 5개의 스래드가 돌아가고 있기 때문에
5씩 count가 증가해야 하지만 Semaphore를 이용해 3개의 스레드만 허용했음으로
3씩 증가하는 것을 볼 수 있습니다.
공유자원 접근을 허용할 스레드의 개수를 1로 하는 경우 Mutex와 동일한 기능을 하게 됩니다.
이 경우를 Binary Semaphore라 하며 위에서 사용한 3개의 스레드를 허용하는 경우를 Counting Semaphore라 합니다.
기능의 유사성 때문에 Binary Semaphore와 Mutex는 같다고도 말할 수 있지만
엄밀히 따지면 Mutex는 Lock기반이며 Semaphore는 Signal기반임으로 다르다고 할 수 있습니다.
이렇게 Thread Safety를 위한 동기화 기법을 알아봤습니다.
마지막으로 Monitor와 Semaphore를 사용하는
서점 예시를 통해 동기화 기법을 정리하고 마무리 하겠습니다.
https://github.com/masiljangajji/Thread_Safety_Library_Example
GitHub - masiljangajji/Thread_Safety_Library_Example: https://masiljangajji-coding.tistory.com/manage/newpost/38?type=post&retur
https://masiljangajji-coding.tistory.com/manage/newpost/38?type=post&returnURL=https%3A%2F%2Fmasiljangajji-coding.tistory.com%2Fmanage%2Fposts - GitHub - masiljangajji/Thread_Safety_Library_Exa...
github.com
서점 조건
- 서점에는 5개의 책이 존재한다.
- 생산자는 5개의 책중 하나를 랜덤하게 납품한다.
- 책은 이름과 번호를 가진다.
- 똑같은 종류의 책 재고는 최대 10권까지 가능하며 , 그 이상 부터는 납품을 미룬다.
- 재고가 없는 경우에는 출고(판매)하기 위해 납품을 기다린다.
- 생산자는 최대 3명까지 허용한다.
- 소비자는 책의 번호별로 구매가능하다.
즉, 여러 사람이 각기 다른 번호의 책을 구매할 경우 동시 구매가 가능하다.
실행 결과
'프로그래밍 기초 > 전산학 기초' 카테고리의 다른 글
[추상클래스 Vs 인터페이스] (0) | 2023.10.22 |
---|---|
클래스 설계와 Abstraction Barrier (0) | 2023.10.22 |
Abstraction(추상화) 기본 개념 - 1편 (0) | 2023.10.20 |
[Static Vs Dynamic] Series (3) | 2023.10.19 |
I/O Stream 기본 개념 (0) | 2023.09.29 |