Closed Jaeeun1083 closed 2 years ago
Thread-safe 즉 쓰레드 안전은 멀티 쓰레드 프로그래밍에서 일반적으로 어떤 함수나 변수 혹은 객체가 여러 쓰레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻합니다.
정확히 말하자면 하나의 함수가 한 쓰레드로부터 호출돼 실행 중일 때, 다른 쓰레드가 그 함수를 호출하여 동시에 함께 실행하더라도 각 쓰레드에서의 함수의 수행 결과가 올바르게 나오는 것입니다.
이렇게 말로만 하면 어려우니 저희가 스터디에서 사용중인 언어인 Java
의 synchronized
로 한 번 보면 좋을 것 같습니다.
sum 변수가 인스턴스 변수로 공유돼고 있는 상태입니다.
이 상태에서 두 개의 쓰레드로 1억을 만들어야한다.
예제를 코드로 만들어보았는데 이때 예상과는 다르게 1억이 아니라 매번 다른 값이 나오게 됩니다.
그 이유는 쓰레드가 공유 변수를 건들면서 동시에 접근을 하다보니 생기는 문제입니다!
이렇게 같은 공유 메모리에 write하는 행위를 Data Race라고도 합니다.
synchronized
키워드를 붙이면 객체가 가진 고유락으로 동시성 문제를 해결할 수 있습니다.이처럼 자바는 동시성 문제를 해결 즉 Thread-safe해주게 만들어주는 방법들이 여러가지가 있습니다. volatile, Atomic 패키지 등등
public class SingletonClass {
private static SingletonClass instance;
public String sampleString = "Sample String";
public SingletonClass() { } // 기본 생성자
public static synchronized SingletonClass getInstance() {
if (instance == null) { // 기존 인스턴스가 존재하지 않는다면 새로 새성
instance = new SingletonClass();
}
return instance; // instance 반환
}
public static void main(String[] args) {
SingletonClass instance1 = getInstance();
SingletonClass instance2 = getInstance();
System.out.println("instance1 = " + instance1); // 같은 해쉬코드 반환
System.out.println("instance2 = " + instance2); // 같은 해쉬코드 반환
System.out.println("우리 둘 싱글톤 맞어? -> " + (instance1 == instance2));
}
}
쓰레드 안전은 다중 쓰레드가 동시에 같은 로직을 실행하는 경우에도 결과가 정확함을 보장한다는 것입니다. 여러 쓰레드가 클래스에 접근할 때 실행 환경이 해당 쓰레드들의 실행을 어떻게 스케쥴 하든 끼워 넣든, 호출하는 쪽에서 추가적인 동기화나 다른 조율 없이도 정확하게 동작하면 해당 클래스는 쓰레드 안전하다고 말합니다.
예시로 풀어서 말하자면 아래와 같습니다.
Data Race
를 막기 위해서 뮤텍스 락으로 보호만 해놨을 뿐입니다.
재진입가능은 다중 쓰레드가 동시에 같은 로직을 실행할 수 있도록 구현해서 Thread-safe를 지원하는 환경입니다.
이 내용도 간단히 풀어서 말하면 재귀호출 했을 때 문제가 생기지 않는 함수를 말합니다.
volatile 키워드를 사용해서 해결할 수 있지만 이 경우에는 하나의 쓰레드만 read하는 상황에서 사용해야 하며 성능도 생각해야 합니다. 즉 쓰레드가 증가 연산자 처럼 필드를 읽고 수정하는 연산을한다면 두 번째 쓰레드가 비집고 들어와서 새로운 값을 저장해버릴수도 있습니다.
3) Atomic
2개의 쓰레드일때 boolean값 flag를 직접 만들어서 Lock을 걸어줄 수 있는 피터슨 알고리즘이 있습니다. 즉 쓰레드마다 flag를 가지고 계속 while문 돌려서 사용가능한지 판별하는 겁니다. 그러나 현재의 CPU는 순차적으로 쓰레드를 실행시키지 않아 피터슨 알고리즘이 먹히지 않습니다. 피터슨 알고리즘과 같이 일반적인 프로그래밍 방식으로는 멀티 쓰레드에서 안정적으로 돌아가는 프로그램을 만들 수 없습니다.
자바에서 생각보다 동시성 때문에 문제가 생기는 경우가 많은데 겪었던 내용들과 어떻게 해결을 했는지도 이야기 해보면 좋을 것 같습니다!
- 그 이유는 쓰레드가 공유 변수를 건들면서 동시에 접근을 하다보니 생기는 문제입니다!
글 재밌게 읽었습니다 ☺️ Thread-safe를 지향하기 위해서는 임계 영역에서의 문제를 해결해야 겠군요! 추가적으로 자바 라이브러리에서는 Reentrant을 기반으로 하는 Lock을 제공하고 있습니다. 그리고 ReentrantLock을 이용해 mutext 방식으로 Thread-safe하게 구현할 수 있습니다.
public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
private ReentrantLock mutex = new ReentrantLock();
@Override
public int getNextSequence() {
try {
mutex.lock();
return super.getNextSequence();
} finally {
mutex.unlock();
}
}
}
결론을 말하자면 모든 Reentrant 함수는 Thread-safe합니다. 하지만, Thread-safe 함수는 Reentrant하지 않습니다. Thread-safe는 여러 쓰레드는 여러 쓰레드에 의해 코드가 실행 되어도 결과가 동등합니다. 그리고 Reentrant는 여러 쓰레드가 코드를 동시에 수행할 수 있고, 결과가 동등합니다. 즉, 동일한 결과를 호출하는 함수는 Thread-safe하지만, 외부 명령에 의해 값이 수정된다면 Reentrant하지 않습니다. 반면에 외부 명령에 의해서도 수정이 되지 않고 동일한 결과를 호출한다면 Reentrant 하게 됩니다.
Thread-safe하게 구현하기 위해서는 임계 영역에서 발생할 수 있는 문제를 해결하면서 구현하는 것입니다. 하지만 락을 이용한 동기화 방식은 병렬 프로그래밍을 단일 프로그램처럼 동작하게 하는 성능상의 이슈가 존재합니다. 그래서 가장 중요한 건 수정이 가능한 공유 자원을 만들지 않는 방식으로 Thread-safe하게 만드는 방식이 좋다고 생각합니다.
비동기적 통신을 하면서 데이터 무결성을 지키기 위해서는 Thread-safe와 Lock을 구현하는 지식으로는 어렵다는 것을 깨닫는 시간이었습니다. 데이터 무결성을 지키기 위해 Reentrant 함수를 만드는 방법을 찾아봐야겠군요.
여러 스레드가 접근했을때, 데이터의 손상이나 바람직하지않는 결과를 방지하는 상황을 말합니다. 해당 객체(혹은 장소)에 여러 스레드가 접근할지의 여부, 동기화를 통해 데이터의 손상이나 바람직하지않는 결과를 방지하고있는지의 여부에 따라서 보장됩니다. 구체적으로 상태가없거나(stateless), 단일연산이 보장되거나, 락을거는등의 방법들로 보장할수있습니다
Reentrant는 특정 스레드가 한번 획득한 락을 다시 확보할수있는것입니다. Reentarnt를 구현하려면, 각 락마다 확보횟수, 확보스레드를 연결시켜야합니다. 스레드가 해제된 락을 확보하면 JVM이 락에대한 소유스레드를 기록하고 확보횟수를 1로지정하고, synchronized블록을 나가면 횟수를 감소시켜 락의 해제를 인식할수있습니다
Reentrant때문의 락의 동작은 쉽게 캡슐화 할수있고, 객체지향 병렬프로그램을 개발하기 단순해졌습니다
public class Stateless implements Servlet {
/**
위의 클래스에 접근하는 특정 스레드는 같은 Stateless클래스에 접근하는 다른스레드의 결과에 영향을줄수없다. => 두 스레드가 상태를 공유하지않기때문에 사실상 서로 다른 인스턴스에 접근하는것과 같다.
/**
* 1. 락은 스레드가 synchronized 블록에 들어가기전에 자동으로 확보되며 정상적으로던,
* 예외가 발생하던 해당 블록을 벗어날때 자동으로 해제된다
* 2. 자바에서 암묵적인 락은 뮤텍스(또는 상호배제 락)로 동작한다. 한번에 한 스레드만 특정락을 소유할수있다.
* 3. 스레드 A가 가지고 있는 락을 스레드 B가얻으려면 A가 해당 락을 놓을때 까지 기다려야한다. A가 락을 놓지않으면 B는 영원히 기다려야한다.
/*
public synchronized void service(ServletRequest req, ServletResponse resp){
...
}
JVM은 volatile로 지정되지않은 long이나 double형의 64비트값에 대해서 메모리에 쓰거나 읽을때 두번의 32비트 연산을 사용할수있도록 허용하여 문제가 될수있습니다
volatile로 선언한 변수의 값을 변경할때, 컴파일러와 런타임 모두 ' 이 변수는 공유해서 사용하고, 실행순서를 재배치 해선 안된다' 라고 인지하여 다른스레드에서 항상 최신의 값을 읽어갈수있도록 해줍니다 (프로세서의 레지스터에 캐시되지않기때문에 캐시값 불일치로인한 문제가 발생하지않게됩니다)
private final ReentrantLock locker = new ReentrantLock();
public void someMethod() {
locker.lock();
try {
//something
} catch (Exception e) {
//some error handle
} finally {
locker.unlock();
}
}
page, 책내용:
질문: