Right now do !

[JAVA] concurrent programming - 교착상태에서 탈출

by 지금당장해

프롤로그

 이전 글에서 교착상태가 무엇인지 이를 재현해보고 교착상태의 thread를 검출해봤다. 이번글은 명시적인 Lock을 이용하여 교착상태에 빠진 thread를 탈출시키는 방법을 제시 해보려 한다.

명시적 Lock의 사용

 JAVA의 명시적인 Lock은 표준 JDK 에서 총 3가지로 제공된다. ReentrantLock, ReentrantReadWirteLock, 그리고 일전에 다루었던 StampedLock이다. 명시적인 Lock은 Java의 Object에 숨어있는 암묵적인 Lock보다 그 야말로 명확하다. 블록이 아닌 lock / unlock의 명시적인 호출로 Lock을 건다. 그렇기에 위험하기도 하다. 

private ReentrantLock lock = new ReentrantLock();
// 중략
lock.lock();
try {
  // Task 정의
} catch (Exception e) {
    
} finally {
    lock.unlock();
}

 위 코드는 synchronized 대체 코드이다. 블럭이 아니기 때문에 finally 블럭에 unlock()을 하지 않을수 있다. (필요하다면) 그렇지만 어디에선가 / 어느시점에서 unlock을 해주지 않으면 문제가 생길것이다. 교착상태 탈출이라는 글에서 명시적인 Lock을 언급한 이유는 교착상태 탈출 방법을 여기서 제공하기 때문이다. 명시적 Lock / 재진입성(Reentrant) Lock에 대해서는 별도로 다루도록 하겠다.

탈출 할 수 있는 lock 함수

 위 예시에서 사용한 lock()이라고 하는 함수는 Dead Lock상황에서 synchronized와 동일한 작용을 한다. 즉 교착상태에서 빠져나갈 방법을 제시하지 못하는다는 뜻이다. 그러나 명시적인 Lock들은 시한부 혹은 interrupt가 가능한 Lock을 제공하여 Dead Lock이나 기아상태로 인해 장시간 wait하는 상황에 대한 해법을 제시한다. 다음 설명은 ReetrantLock Class를 기준으로한 설명이다.

lockInterruptibly()

interrupt() 호출에 의해 InterrupedException이 유발된다.

tryLock() 다른 thread에 의해 Lock이 되어 BLOCKED 상황이 되면 바로 false를 반환한다. 
tryLock(long timeOut, TimeUnit unit) tryLock()과 비슷한데 바로 lock확보 결과를 리턴하는 것이 아니라 지정된 시간 만큼 기다린다. InterruptedException이 유발되는 점에서는 lockInterruptibly() 함수와 유사하다.

 

교착상태에서 탈출

 이전글을 통해 교착상태를 이부러 만들어서 상태를 검출하는 코드를 소개했다. 그 코드를 개선하여 교착상태에 빠진 thread에 예외를 발생시켜 BLOCKED를 탈출하는 코드를 소개하려 한다.

public class DeadLockTestExplicit {

    @Test
    public void runDeadLock() {
        SharedExplicitLock s1 = new SharedExplicitLock();
        SharedExplicitLock s2 = new SharedExplicitLock();

        ExecutorService es = Executors.newFixedThreadPool(3);
        Future<?> future1 = es.submit(new Task(s1, s2));
        Future<?> future2 = es.submit(new Task(s2, s1));

        es.shutdown();

        try {
            int awaitCnt = 0;
            while (!es.awaitTermination(2, TimeUnit.SECONDS)) {
                System.out.println("Not a terminate threads. ");
                reportDeadLockThread();
                awaitCnt++;
                if ( awaitCnt > 3 && !future1.isCancelled() && !future2.isCancelled() ) {
                    // thread pool에서 Interrupt를 호출하는 방법, 작업을 취소하고 Interrupt
                    future1.cancel(true);
                    future2.cancel(true);
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void reportDeadLockThread() {
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        long[] threadIds = bean.findDeadlockedThreads(); // Returns null if no threads are deadlocked.
        if (threadIds != null) {
            ThreadInfo[] infos = bean.getThreadInfo(threadIds);
            for (ThreadInfo info : infos) {
                String s = info.getThreadName();
                System.out.println(String.format("DeadLocked Thread Name >>>> %s", s));
            }
        }
    }

    static class SharedExplicitLock
    {
    	// 명시적인 재진입성 Lock을 선언한다.
        private ReentrantLock lock = new ReentrantLock();

        void test1(SharedExplicitLock s2)
        {
            try {
                // Lock을 건다. 그런데 BLOCKED상태에서 intrrupt() 예외처리 할 수 있다.
                lock.lockInterruptibly();
                System.out.println("test1-begin");
                ThreadUtil.sleep(1000);

                s2.test2(this);
                System.out.println("test1-end");

            } catch (InterruptedException e) {
                System.out.println(String.format("Release Thread >>> %s"
                    , Thread.currentThread().getName()));
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }

        void test2(SharedExplicitLock s1)
        {
            try {
                lock.lockInterruptibly();

                System.out.println("test2-begin");
                ThreadUtil.sleep(1000);
                s1.test1(this);
                System.out.println("test2-end");

            } catch (InterruptedException e) {
                System.out.println(String.format("Release Thread >>> %s"
                    , Thread.currentThread().getName()));
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }

    static class Task implements Runnable
    {
        private SharedExplicitLock s1;
        private SharedExplicitLock s2;

        Task(SharedExplicitLock s1, SharedExplicitLock s2)
        {
            this.s1 = s1;
            this.s2 = s2;
        }

        @Override
        public void run()
        {
            s1.test1(s2);
        }
    }
}

에필로그

 이번 글이 Dead Lock을 인정하고 그런 상황도 벌어질 수 있으니 대충 짜고 Dead Lock 검출해서 interrupt() 호출하여 예외 처리로 빠져 나가자는 주장이 아님을 명확히 하고자 한다. 개발자는 동기화 구간이 존재하는 코드에서 또 다른 동기화 구간이 존재하는 프로그램을 호출할 때 상호 Lock상태 호출에 의한 교착 가능성에 대해 검토해야 하고 이를 회피하는 전략을 기본으로 삼아야 한다. 그러나 사람이 하는 일이 늘 완벽하지 못하다 보니 이를 감시하고 처리하는 방법까지 고민하게 되었다. 

 

 

블로그의 정보

지금 당장 해!!!

지금당장해

활동하기