2013-04-25 1 views
2

아주 간단한 Java 코드로 이상한 문제가 있습니다.간단한 Java 코드에서 로깅 문으로 교착 상태가 발생합니까?

나는 프로그램의 라이센스를 제어하고 동적으로 제어 할 수있는 새로운 클래스를 요청할 수 있습니다 (예 : 새로운 라이센스는 주문형으로 발행 될 수 있음). LicenseManager은 물론 스레드 잠금이 필요하므로 큰 잠금 장치를 사용합니다. 그러나 사과를 제어하는 ​​데 사용하는 잠금 장치가 하나 더 있습니다.이 교착 상태에 어떻게 든 관련이 있다고는하지만 실제로 어떻게 볼 수는 없습니다. 이 프로그램은 또한 엔터프라이즈 로깅 기능을 지원하는 로깅 처리기를 설치합니다.

그냥 코드 LicenseManager을 보면 그 중 하나의 문제는 볼 수 없습니다.

import java.util.logging.*; 

public class Main { 
    public static final LicenseManager licenseManager = new LicenseManager(); 

    static class LicenseManager { 
     private static final Logger logger = 
       Logger.getLogger(LicenseManager.class.getName()); 

     class InsufficientApplesException extends RuntimeException {}; 
     class InsufficientPiesException extends RuntimeException {}; 

     private int apples = 10; 
     private int pies = 10; 
     private boolean enterpriseLoggingEnabled = true; 

     // Apples have high contention; so they get their own lock. 
     private final Object appleLock = new Object(); 

     void useApple() { 
      checkExpired(); 
      synchronized (appleLock) { 
       logger.info("Using apple. Apples left: " + apples); 
       if (apples == 0) throw new InsufficientApplesException(); 
       apples--; 
      } 
     } 

     /* Examples. There are lots of other operations like this 
     * on LicenseManager. We don't have time to prove that 
     * they are commutative, so we just use the main object lock 
     * around them all. The exception is when we find one with high 
     * contention, for example apples. */ 
     synchronized void usePear() {checkExpired(); /*...*/} 
     synchronized void checkExpired() {} 

     synchronized void usePie() { 
      checkExpired(); 
      logger.info("Using pie. Pies left: " + pies); 
      if (pies == 0) throw new InsufficientPiesException(); 
      boolean reallyCanUsePie = true; // do expensive pie computation 
      if (reallyCanUsePie) { 
       useApple(); /* using a pie requires an apple. 
          * TODO: stop putting apples in the pumpkin pie */ 
       pies--; 
      } 
     } 

     synchronized boolean isEnterpriseLoggingEnabled() { 
      return enterpriseLoggingEnabled; 
     } 
    } 

    public static void main(String[] args) { 
     // Install enterprise log handler on the root logger 
     Logger.getLogger("").addHandler(new Handler() { 
      @Override 
      public void publish(LogRecord lr) { 
       if (licenseManager.isEnterpriseLoggingEnabled()) 
        System.out.println("ENTERPRISE ALERT! [" 
          + lr.getLevel() + "] " + lr.getMessage()); 
      } 
      @Override public void flush() {} 
      @Override public void close() throws SecurityException {} 
     }); 

     // Simulate fat user 
     new Thread() { 
      @Override 
      public void run() { 
       while (true) { 
        licenseManager.usePie(); 
       } 
      } 
     }.start(); 

     // Simulate fat albeit healthy user 
     while (true) { 
      licenseManager.useApple(); 
     } 
    } 
} 

그리고 내가 그것을 실행하면

$ java Main 
Apr 25, 2013 3:23:19 PM Main$LicenseManager useApple 
INFO: Using apple. Apples left: 10 
Apr 25, 2013 3:23:19 PM Main$LicenseManager usePie 
INFO: Using pie. Pies left: 10 
ENTERPRISE ALERT! [INFO] Using pie. Pies left: 10 

당신은 모두 원형 먹는 사람을 기대는 파이의 부족으로 사망, 대신 두 스레드 교착 상태.

흥미롭게도 로킹 라인을 useApple (logger.info("Using apple. Apples left: " + apples);)으로 제거하면 교착 상태가 발생하지 않습니다 (모든 파이가 사용되기 전에 모든 사과가 사라 졌기 때문에 "파이 사용"으로 스팸 된 로그를 보지 못합니다).) :

$ java Main 
Exception in thread "main" Main$LicenseManager$InsufficientApplesException 
    at Main$LicenseManager.useApple(Main.java:24) 
    at Main.main(Main.java:79) 
Apr 25, 2013 3:23:42 PM Main$LicenseManager usePie 
INFO: Using pie. Pies left: 10 
ENTERPRISE ALERT! [INFO] Using pie. Pies left: 10 
Exception in thread "Thread-1" Main$LicenseManager$InsufficientApplesException 
    at Main$LicenseManager.useApple(Main.java:24) 
    at Main$LicenseManager.usePie(Main.java:43) 
    at Main$2.run(Main.java:72) 

왜? 로깅을 제거하지 않고 어떻게 해결할 수 있습니까? 메인 스레드는 (하나의 사과를 먹는)를 appleLock을 소유하고 synchronized isEnterpriseLoggingEnabled() 방법과 자식 스레드 (파이를 먹는 일)이 licenseManager 객체를 소유하고 usePie()의 내부 useApple() 호출에 액세스하려고하기 때문에

+0

정말 교착 상태에 빠졌습니까? 스택 트레이스에서 교착 상태를 본 적이 있습니까? – Gray

+0

'while (true)'에 약간의'Thread.sleep()'을 삽입 해 보았습니까? – Fildor

+0

FYI : 로거가 프로그램의 동기화 패턴을 변경하는 커버 아래에 동기화 된 'PrintStream'을 가지고있을 수 있습니다. – Gray

답변

2

문제함으로써이 문제를 해결할 수 있습니다 log.info() 통화가 Handlerpublish() 방법으로 끝나고 licenseManager.isEnterpriseLoggingEnabled()licenseManager에 잠금 장치가 필요합니다.

그래서이 코드를 완료 할 수 없습니다 :

synchronized (appleLock) { 
     //this line requires a lock on license manager, and this lock is not 
     //available because is hold by the other thread waiting to get the appleLock 
     logger.info("Using apple. Apples left: " + apples); 

     if (apples == 0) throw new InsufficientApplesException(); 
     apples--; 
    } 

하나는 교착 상태를 제거하는 쉬운 방법은 isEnterpriseLoggingEnabled() 방법에 동기화를 제거하는 것입니다. 속성이 읽기 전용이므로 enterpriseLoggingEnabled 필요하지 않습니다.

+0

wait ... 그래서 로그 처리기 콜백은'log.info()'호출과 같은 쓰레드에서 수행됩니까? – Dog

+0

그래, 왜 쓰레기를 낭비하거나 컨텍스트 스위치를 만드는거야. – dcernahoschi

+0

그래, 그들이 동일한 스레드에있는 것 같습니다 : https://ideone.com/Z6Dqgr. Java에서 로깅 작업에 모든 로그가 이동하는 대기열이있는 스레드가 있다고 생각한 다음 해당 스레드에서 핸들러가 실행됩니다. 나는'Logger '에 대한 문서가 스레드로부터 안전하다고 말했기 때문에 이것을 생각했다. 핸들러가 같은 스레드에서 호출되는지 여부는 어딘가에 문서화되어 있습니까? 로깅을 제거하지 않고 코드를 수정하려면 어떻게해야합니까? – Dog

2

그들은 교착 상태에 들어가 (따라서 appleLock이 필요합니다).

더 이상 복용 후 licenseManager 내에서 동기화 된 메소드를 호출하기 때문에 교착 상태는 Logger 문없이 발생하지 않는 appleLock

당신은 아마 isEnterpriseLoggingEnabled하지 synchronized

+0

'useApple'은'checkExpired()'를 통해'licenseManager' 잠금에 접근 한 다음 ** release **로'appleLock'을 얻습니다. 여기서'licenseManager'를 잠그지 않습니다. – Dog

+0

@Dog 디버거를 사용하여 테스트했습니다. 나는 내 대답이 어떻게 될지 100 % 확실하다. – durron597

+0

'useApple'에 로깅이 없으면 어떻게 교착 상태가 발생할 수 있습니까? 교착 상태는'isApplication'의 로깅에 기인한다고 생각합니다. 왜냐하면'isEnterpriseLoggingEnabled'를 다시 호출하는 핸들러를 호출하기 때문입니다.이 핸들러는 두 개의 잠금을 즉시 얻으려고합니다. – Dog

0

이유 :

많은 동기화 방법이 있습니다. 하지만 그것들은 모두 Main의 하나의 모니터 인스턴스에서 동기화되므로 메소드 중 2 개를 동시에 호출하여 교착 상태에 빠지게됩니다. 이를 모두 정렬하려면 코드의 다른 부분에 대해 별도의 모니터 (잠금 개체)를 만들어야합니다. 사과 (appleLock)에 대해 별도의 잠금을 설정 했는데도 이유가 무엇이든 Main instance에 계속 동기화됩니다.당신이 isEnterpriseLoggingEnabled()에 동기화가 필요하지 않는 이유는

: 당신이 enterpriseLoggingEnabled=!enterpriseLoggingEnabled;처럼 설정하지 않으면, 경합가 있더라도

, 동기화하지에 괜찮습니다. 읽기만하면 값을 업데이트하면됩니다. 이를 수행하려면 enterpriseLoggingEnabled를 volatile으로 만드십시오. 이렇게하면 불필요한 잠금 및 전체 잠금 문제가 제거됩니다. 여기에 자물쇠가 있어야하거나 다른 소비자가있는 다른 방법으로 자물쇠가 필요한 경우 별도의 자물쇠를 만드십시오. 어쩌면 ReentrantLock 일 수 있습니다.