ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java Multithreading] 동기화의 종류와 교착상태Dead Lock
    Java/Multi Threading 기초 2022. 12. 1. 14:35

    Java Multithreading 시리즈

    안녕하세요 🤗 오늘은 동기화의 종류와 교착상태에 대한 예시를 이야기 해보려고 합니다.아마 익숙한 내용이 될 것 같네요!

    세밀하게fine-grained 다루는 락

    동기화를 하는 방법 중 가장 쉬운 건 공유 리소스 전체에 락을 거는 것입니다. 이런 방식을 세밀하다fine-grained고 표현합니다.
    이에 대한 장단점은 앞선 포스팅에서 많이 다루었기 때문에 아주 간략히만 이야기 하면, 각 실행 스레드가 모든 공유 자원을 사용하는 최악의 경우에는 마치 싱글 스레드 프로그램처럼 느리게(근데 이제 컨텍스트 스위칭을 곁들인) 동작한다는 것이죠. 항상 최악의 경우로 동작하는 건 당연히 아니지만요.

    성긴coarse-grained 락

    다른 한 가지 방식은 공유자원 별로 락킹 하는 것입니다. 이런 방식을 성긴coarse-grained 방식이라고 합니다. 때로는 낙천적인 락이라고 표현하기도 하지만, 개인적으로는 의미가 잘 드러나는 표현은 아닌 것 같아요.
    어떻게 보면 가장 이상적으로 동작하는(모든 스레드가 실행 상태를 최대한 유지하는 것) 방식이지만, 관리가 어렵고, 최악의 경우에는 교착상태Dead Lock가 발생합니다.

    교착상태Dead Lock의 예시

    교착상태, 데드락이란 뭘까요? 이름만 봤을 때는 뭔가... 꼼짝할 수 없이 대치하고 있는 상태를 말하는 것 같은데 말이죠.

    코드로 설명하기 전에 예시를 들어보겠습니다. 교차로의 네 방향에서 차 4대가 진입하고 있습니다. 이 교차로는 신호도 없고 여러 대의 차가 동시에 지나갈 수 없는 좁은 길입니다. 그런데 각 방향의 자동차가 동시에 교차로에 머리를 들이밀었습니다. 마치 바람개비와 같은 모양으로 서로 끼어있는거죠. 그리고 말합니다. "니가 먼저 움직이면 내가 움직여서 비켜줄게." 하지만 그 상태에서는 아무도 움직일 수가 없습니다(후진은 예외...😅). 이것이 교착상태입니다.
    다른 경우로 하나만 더 예를 들어볼게요. 이번에는 교차로 위에 차가 두 대 있습니다. 한 대는 위에서 아래로, 한 대는 왼쪽에서 오른쪽으로 움직입니다. 동시에 교차로에 진입하려고 하는 상태입니다. 그런데 두 운전자는 저번에 된통 끼었던 기억이 떠올랐습니다. 그래서 교차로 앞에 서서 서로에게 말합니다. "니가 기다리고 있으면 내가 먼저 움직일게." 하지만 서로 먼저 기다리기를 요청하다보니 결과적으로 둘 다 움직일 수 없는 상태가 됩니다. 이것도 교착상태입니다.

    이제 코드로 살펴볼게요. 다만 조금 간략화 된 코드로 보겠습니다!

    class Intersection {
        Road updown = new Road();
        Road leftright = new Road();
    
        takeUpdownRoad() {
            synchronized(updown) {
                synchronized(leftright) {
                    goingDown();
                }
            }
        }
    
        takeLeftrightRoad() {
            synchronized(leftright) {
                synchronized(updown) {
                    goingRight();
                }
            }
        }
    }
    
    class CarUpdown implements Runnable {
        Intersection intersection;
        public CarUpdown(Intersection intersection) {
            this.intersection = intersection;
        }
        @Override 
        public void run() {
            intersection.takeUpdownRoad();
        }
    }
    
    class CarLeftright implements Runnable {
        Intersection intersection;
        public CarLeftright(Intersection intersection) {
            this.intersection = intersection;
        }
        @Override 
        public void run() {
            intersection.takeLeftrightRoad();
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Intersection intersection = new Intersection();
            Thread updownThread = new Thread(new CarUpdown(intersection));
            Thread leftrightThread = new Thread(new CarLeftright(intersection));
    
            updownThread.start();
            leftrightThread.start();
        }
    }

    위에 말한 두 번째 예시와 동일한 코드라는 것을 알 수 있죠?
    위아래의 길을 a, 왼쪽에서 오른쪽으로의 길을 b라고 할게요. CarUpdown은 a에 대한 락을 얻고 b의 락을 얻으려하고 있고, CarLeftright는 b의 락을 얻은 뒤 a의 락을 얻으려고 합니다. 그런데 각 차가 먼저 자신의 길에 대한 락을 얻고, 상대의 길에 대한 락을 얻으려고 하는 상황이 벌어진다면? 교착상태가 일어나게 되는 것이죠.

    결론

    프로그래머는 세밀한 동기화와 성긴 동기화 중 필요에 맞게 선택할 수 있고, 만약 성긴 동기화를 시도한다면 교착상태가 일어나지 않도록 주의해야 합니다.

    다음 포스팅 부터는 교착상태의 구체적인 조건과 해결 방안에 대해 알아보도록 하겠습니다.
    감사합니다. 🥰

    댓글

Designed by Tistory.