ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java Multithreading] 임계 영역과 동기화
    Java/Multi Threading 기초 2022. 10. 28. 10:34

    Java Multithreading

    멀티스레딩의 개념과 Java에서의 활용법을 공부하고 정리하는 시리즈입니다.


    여러 스레드가 하나의 공유되는 자원에 원자성이 보장되지 않는 작업을 시도할 경우 병행성 문제가 발생한다고 지난 포스팅에서 이야기 했습니다.
    오늘은 Java에서 해당 문제를 해결하는 방법 중 하나인 synchronized 키워드에 대해 이야기 해보겠습니다.

    method block에 정의하기

    synchronized를 정의하는 두 가지 방법 중 하나는 메서드 블럭에 정의하는 것입니다.

    public class Counter {
        public int count;
        public Object obj;
    
        public synchronized void increase() {
            count++;
        }
    
        public synchronized void decrease() {
            count--;
        }
    }

    위 코드에서 synchronized가 붙은 메서드들을 공통적으로 부를 수 있는 이름이 떠오르시나요? 위의 두 메서드가 바로 임계영역입니다. 동시에 synchronized를 붙임으로써 임계영역을 한 번에 걸어잠글 수 있는 락(lock)을 제공합니다. 이제 여러 스레드 중 하나의 스레드가 먼저 임계영역 안으로 들어오면 락이 걸리고, 나머지 스레드들은 먼저 들어간 스레드가 작업을 마치고 임계영역 밖으로 나오기 전까지 대기하게 됩니다. 락의 주체는 해당 인스턴스(this)가 되고 영역은 메서드 전체가 되는 것이죠.

    이렇게 메서드 블럭에 synchronized를 붙여서 락을 거는 이러한 방식을 모니터monitor라는 단어로 지칭합니다.

    synchronized block 만들기

    synchronized 키워드를 사용한 두 번째 방법은 동기화 블럭을 만드는 방법입니다.

    public class Counter {
        public int count;
        public Object obj;
    
        public void increase() {
            synchronized(obj) {
                count++;
            }
        }
    
        public void decrease() {
            synchronized(obj) {
                count--;
            }
        }
    }

    이렇게, 임계영역이라고 판단되는 부분에 각각 별도로 동기화 블럭을 만들 수 있습니다. 다양한 메서드가 있다면 각 메서드는 비동기 방식으로 수행되면서 임계영역에서만 락을 걸어 공유 리소스의 무결성을 보장받을 수 있게 되었습니다. 이 방식에서 락의 주체는 공유될 자원, 파라미터로 넘겨지고 있는 obj 객체입니다. 어떤 객체든 이 블럭의 파라미터로 넘겨질 수 있습니다. 만약 두 개의 동기화 블럭이 obj1, obj2와 같은 식으로 별개의 자원을 락으로 사용하는 상황에서 obj1, obj2 각각만을 사용하는 스레드a, 스레드b가 접근한다면, 동기화 블럭 안에 있지만 별개의 동기화 블럭이기 때문에 서로 대기하도록 제한하지는 않게 됩니다.

    결과적으로 모니터 방식 보다는 좀 더 유연하고 세분화 된 락킹이 가능합니다. 물론 그것은 장점 뿐만이 아니라 단점이기도 합니다. '유연하고 세분화' 되었다는 건 나쁘게 말하면 '관리가 어렵다'는 뜻이니까요. 임계영역으로 접근하기 위해 스레드 마다 별개로 생성된 공유 리소스 인스턴스에 접근하는 것인지, 락킹 객체가 별개의 것인지 아닌지 잘 확인해야 합니다.

    재진입성, 재진입 가능(Reentrant)

    중요한 개념 하나를 설명하면서 오늘 포스팅을 마무리하려고 합니다. Reentrant라는 개념입니다. 이 개념은 Thread-safe와 헷갈리기 쉽습니다.
    reentrant의 주요한 특성은, 여러번을 호출했을 때 하나의 호출이 그 밖의 호출의 결과에 영향을 미치지 않는다는 말입니다. 각각의 호출은 다른 어떤 것의 영향도 받지 않고 자신만의 결과를 도출합니다. 심지어는 한 스레드가 여러번 호출하는 상황을 상정해도 말이죠.

    우리가 지금까지 설명하고 이해해온 메서드들은 전부 Thread-safe하게 되어있습니다. 다시 말해 멀티스레딩 프로그램에서 안전합니다. 하지만 재진입(Reentrant)이 가능한 메서드인가를 따질 때에는 몇 가지 조건을 더 고려할 필요가 있습니다. Reentrant의 엄격한 조건은 다음과 같습니다.

    1. 공유 리소스를 사용해서는 안됩니다. 클래스 변수와 멤버 변수를 사용하면 안됩니다.
    2. 레퍼런스를 반환해서는 안됩니다.
    3. arguments와 local variable로만 작업해야 합니다(1번의 연장선입니다).
    4. 싱글톤 리소스와 연관된 락이어서는 안됩니다.
    5. Reentrant 하지 않은 동작을 호출하지 말아야 합니다.

    일견 복잡해보이지만 공통적으로 이야기하는 부분은, stack만을 사용하는 thread-safe한 메서드만이 reentrant하다는 뜻입니다.
    위에도 말했지만 이 포스팅에서 저희가 본 메서드들은 전부 reentrant 합니다. 즉 thread-safe 합니다. 하지만 reentrant의 조건을 고려해보면, 역으로 thread-safe한 동작이라고 해서 모두 reentrant하지 않다는 것까지 이해할 수 있습니다.

    참고로, Java API에서는 ReentrantLock이라는 클래스를 별도로 제공하기도 합니다.

    오늘 내용은 쉬우면서도 어려웠던 것 같네요. 하지만 다음 내용이 더 어려울 것 같습니다😅
    그러면 기대되는 마음으로 다음 포스팅에서 뵙겠습니다. 😇

    댓글

Designed by Tistory.