On 09/05/2013 02:43 PM, Daniel Fuchs wrote:
On 9/5/13 1:51 PM, Peter Levart wrote:
Threads that call ensureLogManagerInitialized() after that will find
the flag
is true before entering the synchronized block and will return
directly.
...and such threads can see rootLogger field being either still null or
not null but the Logger instance that is obtained via such data race can
be half-initialized.
I don't think so, because the flag isn't set to true until
the initialization is finished.
BTW - there was a reason for having both an initializationCalled
*and* a synchronized at method level. The purpose of
initializationCalled flag was to prevent infinite recursion in the
same thread - because addLogger within ensureLogManagerInitialized
will trigger a new call to ensureLogManagerInitialized that the
synchronized won't block - since it's in the same thread.
Oh, I see. The ensureLogManagerInitialized() is also called from
requiresDefaultLoggers() which is called from addLocalLogger(Logger)
which is called from addLogger(Logger) which is also called from
ensureLogManagerInitialized().
I suspect that calling ensureLogManagerInitialized() from
requiresDefaultLoggers() is not needed (any more). Why?
- the ensureLogManagerInitialized() is only meant for the global
LogManager (LogManager.manager). It has no effect on private LogManagers.
- almost all accesses to the global LogManager are done via
LogManager.getLogManager() which already calls
ensureLogManagerInitialized() before returning the LogManager.manager
instance.
- the only other places where private LogManager.manager field is
accessed directly are:
- in the ensureLogManagerInitialized() method itself to skip
initialization for private LogManagers
- in the LoggerContext.requiresDefaultLoggers() to check if the
owner is the global LogManager (an reference equality check)
- in the Cleaner.run() to prevent premature GC of global LogManager
All those direct LogManager.manager field usages do not call any methods
on the so obtained global LogManager, so I conclude that any code that
calls any instance method on global LogManager must have obtained it
from the LogManager.getLogManager() method and this method already
ensures that it is initialized.
If you remove a call to ensureLogManagerInitialized() from
LoggerContext.requiresDefaultLoggers() then there is one less
possibility for recursion. The other is in Logger.getGlobal(). Is this
needed? I think not:
public static final Logger getGlobal() {
if (global != null && global.manager == null) {
global.manager = LogManager.getLogManager();
}
if (global.manager != null) {
global.manager.ensureLogManagerInitialized();
}
return global;
}
Static initialization of Logger class now has no dependency on
LogManager, so there should be no recursion in static initialization.
The 1st call to Logger.getGlobal() should find global != null and
global.manager == null and so LogManager.getLogManager() should be
called but that call does not recurse because LogManager obtains the
global Logger directly from the deprecated Logger.global field, so the
global.manager should be assigned with the initialized global
LogManager. No need to call global.manager.ensureLogManagerInitialized()
later.
The method could simply be:
public static final Logger getGlobal() {
if (global.manager == null) {
global.manager = LogManager.getLogManager();
}
return global;
}
Now that both recursive calls to ensureLogManagerInitialized() are
removed, there's no need to check for recursive invocation in the
ensureLogManagerInitialized() and initializationCalled flag can be removed.
So I need in fact two flags: one initializationDone - which will
be volatile - the other initializationCalled - which doesn't need
to be since it will be always written/read from within the
synchronized block.
I toyed with the idea of using a single volatile three valued byte
instead:
initialization: 0 => uninitialized, 1 => initializing, 2 => initialized
I opted for the two booleans - but the three valued byte might be more
efficient:
1)
volatile byte initialization = 0;
ensureLogManagerInitialized() {
if (initialization == 2) return; // already done
synchronized(this) {
if (initialization > 0) return;
initialization = 1; // avoids recursion
try {
// do stuff which might recurse on
// ensureLogManagerInitialized()
} finally {
initialization = 2; // mark as done
}
}
}
vs 2)
volatile boolean initializationDone = false;
boolean initializationCalled = false;
ensureLogManagerInitialized() {
if (initializationDone) return; // already done
synchronized(this) {
if (initializedCalled || initializationDone) return;
initializedCalled = true; // avoids recursion
try {
// do stuff which might recurse on
// ensureLogManagerInitialized()
} finally {
initializationDone = true; // mark as done.
}
}
}
Don't know if you have a preference for one or the other.
My current impl (still under test) is 2)
After re-checking the JMM cookbook
(http://gee.cs.oswego.edu/dl/jmm/cookbook.html), I see that you were
right in the following:
volatile boolean initializationDone;
Logger rootLogger;
Thread1:
rootLogger = new RootLogger();
initializationDone = true;
Thread2:
if (initializationDone) {
...
Logger logger = rootLogger;
...use logger...
}
A normal write followed by volatile write can not be reordered.
A volatile read followed by normal read can not be reordered either.
So if every read access of rootLogger field is performed via some call
to LogManager instance method and an instance of global LogManager can
only be obtained via LogManager.getLogManager() which calls
ensureLogManagerInitialized(), the synchronization should be OK.
Regards, Peter
best regards,
-- daniel
I can make rootLogger volatile. I'm not 100% sure it is required - but
I think
it is more correct, and as such it will make the code more
maintainable.
Good point. It's probably something I would have overlooked.
I think volatile rootLogger field is required for correct publication of
RootLogger instances if double-checked locking is used...
Regards, Peter