독서일지/클린 코드

클린 코드 독서일지 - Day 44

Sadie Kim 2023. 12. 21. 12:56

가능한 실행 경로

  • 다음의 incrementValue 메서드가 있는 IdGenerator 클래스가 있다고 하자.
public class IdGenerator {
    int lastIdUsed;

    public int incrementValue() {
        return ++lastIdUsed;
    }
}
  • 스레드 하나가 IdGenerator 인스턴스 하나를 사용한다고 가정하면 가능한 실행 경로+결과는 하나다.

  • 하지만 만약 IdGenerator 인스턴스를 스레드 두 개가 호출한다면 다음 결과들이 모두 가능하다.

    • 스레드 1이 94를 얻고, 스레드 2가 95를 얻고, lastIdUsed가 95가 됨
    • 스레드 1이 95를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 95가 됨
    • 스레드 1이 94를 얻고, 스레드 2가 94를 얻고, lastIdUsed가 94가 됨
  • 이는 가능한 실행 경로 수와 JVM의 동작 방식에 의한 결과

경로 수

  • return ++lastIdUsed라는 자바 코드 한 줄은 바이트 코드 명령 8개에 해당한다.
  • 따라서 두 스레드가 명령 8개를 뒤섞어 실행할 가능성이 충분하다.
    => 계산하면 가능한 경로 수가 12,870개임(lastIdUsed가 long 정수라면 읽기/쓰기 명령이 두 단계로 실행되므로 가능한 경로 수가 2,704,156개)
  • 메서드에 synchronized 키워드를 추가하면 스레드가 N개일 때 가능한 경로 수가 N!개로 줄어든다.

심층 분석

  • 원자적 연산 : 중단이 불가능한 연산. ex) 자바 메모리 모델에서 32비트 메모리에 값을 할당하는 연산은 중단이 불가능하므로 원자적이다.
  • 원자적 연산일 경우, 스레드가 가능한 경로 수가 많더라도 가능한 결과가 단 하나이므로 예상치 못한 동작을 할 일이 없다.
  • 원자적 연산이 아닐 경우, 연산을 실행하는 도중에 다른 스레드가 끼어들어 연산에 사용하는 값을 변경할 수가 있다.
    할당 연산일 경우 콜 스택에 상수를 저장하므로 서로 간섭하더라도 결과가 달라지지 않는다.
    그러나 전처리 증가 연산자(++)와 같은 증가 연산일 경우 콜 스택에 기존 값을 가져온 후 증가시키므로, 다른 스레드가 연산 도중 끼어들 경우(이미 값을 가져왔는데 끼어들어 연산을 마치고 나갈 경우) 다른 스레드의 증가 연산은 이미 가져온 값에 반영되지 않아 작업을 덮어쓰게 된다.

결론

어떤 연산이 안전하고 안전하지 못한지 파악할 만큼 메모리 모델을 이해하고 있어야 한다.
다음을 알아야 한다.

  • 공유 객체/값이 있는 곳
  • 동시 읽기/수정 문제를 일으킬 소지가 있는 코드
  • 동시성 문제를 방지하는 방법