3

我对一些相当简单的 Java 代码有一个奇怪的问题。

我有一个类LicenseManager,它控制程序的许可,并且可以动态控制(可以按需发布新许可等)。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);不会发生死锁(您不会看到日志中充斥着“using pie”,因为在任何 pie 可以使用之前,所有的苹果都碰巧消失了):

$ 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)

为什么?如何在不删除日志记录的情况下解决此问题?

4

3 回答 3

2

他们陷入僵局,因为主线程(吃苹果的人)拥有appleLock并试图访问该synchronized isEnterpriseLoggingEnabled()方法,而子线程(吃馅饼的人)拥有licenseManager对象并useApple()在其中调用usePie()(因此需要appleLock)。

没有该Logger语句就不会发生死锁,因为它不再需要在执行licenseManager后调用同步方法appleLock

您可能可以通过设置isEnterpriseLoggingEnablednot来解决此问题synchronized

于 2013-04-25T21:56:13.843 回答
2

问题是您的log.info()调用最终在Handler'publish()方法中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属性是只读的。

于 2013-04-25T22:15:13.330 回答
1

为什么会这样:

你有很多同步方法。但它们都在一个监视器上同步 - main 的 youк 实例,因此您通过同时调用 2 个方法来使自己陷入僵局。为了解决所有问题,您必须为代码的不同部分制作单独的监视器(锁定对象)。您确实为苹果 ( ) 设置了单独的锁,但无论出于何种原因appleLock,您仍然会在其上方同步。Main instance

为什么你不需要同步isEnterpriseLoggingEnabled()

尽管存在争用,但如果您不将其设置为enterpriseLoggingEnabled=!enterpriseLoggingEnabled;,则可以不同步。您只需要在读取时更新值。为此,请创建 enterpriseLoggingEnabled volatile。这应该消除不必要的锁定和整个锁定问题。如果您真的需要这里的锁或其他满足所有消费者的方法,请为它制作一个单独的锁。也许一个ReentrantLock.

于 2015-02-13T06:40:23.380 回答