我对一些相当简单的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);
)中的日志记录行会导致死锁不会发生(您没有看到使用“使用饼图”垃圾邮件的日志,因为所有的苹果都恰好是在任何馅饼可以使用之前消失了):
$ 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)
为什么呢?如何在不删除日志记录的情况下解决此问题?
答案 0 :(得分:2)
他们陷入僵局,因为主线程(吃苹果的人)拥有appleLock
并试图访问synchronized isEnterpriseLoggingEnabled()
方法而子线程(吃馅饼的人)拥有{ {1}}对象并在licenseManager
内调用useApple()
(因此需要usePie()
)。
没有appleLock
语句就不会发生死锁,因为在获取Logger
licenseManager
内调用同步方法
您可以通过appleLock
而非isEnterpriseLoggingEnabled
答案 1 :(得分:2)
问题是,log.info()
的{{1}}调用最终会导致Handler
需要锁定publish()
的{{1}}方法。
所以这段代码无法完成:
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--;
}
属性是只读的。
答案 2 :(得分:0)
为什么会这样:
你有很多同步方法。但是它们都在一个监视器上同步 - 你是Main的实例,所以你通过同时调用其中的两个方法来使自己陷入僵局。要对其进行排序,您必须为代码的不同部分制作单独的监视器(锁定对象)。您确实拥有单独的苹果锁(appleLock
),但无论出于何种原因,它仍会在Main instance
上方同步。
为什么您不需要isEnterpriseLoggingEnabled()
上的同步:
尽管存在争用,但如果您没有将其设置为enterpriseLoggingEnabled=!enterpriseLoggingEnabled;
,则可以不同步。您只需更新读取值即可。为此,请创建enterpriseLoggingEnabled volatile
。这应该消除不必要的锁定和整个锁定问题。如果你真的需要这里的锁或其他方法与aplle消费者的内容,请为它单独锁定。也许是ReentrantLock
。