As we know, ReentrantLock
has a max reentrant limit: Integer.MAX_VALUE
; Does synchronized
block have reentrant limit too?
Update: I found it is hard to write test code for synchronized reentrant:
public class SyncReentry {
public static void main(String[] args) {
synchronized (SyncReentry.class) {
synchronized (SyncReentry.class) {
// ...write synchronized block for ever
}
}
}
}
Can anyone help write some code for synchronized reentrant limit test?
Since the specification does not define a limit, it’s implementation specific. There doesn’t even have to be a limit at all, but JVMs are often optimized for high performance, considering the ordinary use cases rather than focusing on support for extreme cases.
As said in this answer, there’s a fundamental difference between an object’s intrinsic monitor and a
ReentrantLock
, as you can acquire the latter in a loop, which makes it necessary to specify that there’s limit.Determining the actual limit of a particular JVM implementation, like the widely used HotSpot JVM, has the problem that there are several factors which can affect the result, even in the same environment.
To experiment with the actual implementation, I used the ASM library to generate bytecode which acquires an object’s monitor in a loop, an action, ordinary Java code can not do
On my machine, it printed
in one run, but different numbers in the same order of magnitude in other runs. The limit we’ve hit here, is not a counter, but the stack size. E.g. re-running this program in the same environment, but with the
-Xss10m
option, gave ten times the number of lock acquisitions.So the reason why this number is not the same in every run, is the same as elaborated in Why is the max recursion depth I can reach non-deterministic? The reason why we don’t get a
StackOverflowError
is that the HotSpot JVM enforces structured locking, which means that a method must release the monitor exactly as often as it has acquired it. This even applies to the exceptional case and as our generated code does not make any attempt to release the monitor, theStackOverflowError
gets shadowed by anIllegalMonitorStateException
.Ordinary Java code with nested
synchronized
blocks can never get anywhere near 60,000 acquisitions in one method, as the bytecode is limited to 65536 bytes and it takes up to 30 bytes for ajavac
compiledsynchronized
block. But the same monitor can get acquired in nested method invocations.For exploring the limits with ordinary Java code, expanding the code of your question is not so hard. You just have to give up indenting it:
The method will invoke itself to have a number of nested acquisitions matching the number of nested invocation times the number of nested
synchronized
blocks within method.When you run it with the small number of
synchronized
blocks as above, you’ll get aStackOverflowError
after a large number of invocations, which changes from run to run and is affected by the presence of options like-Xcomp
or-Xint
, indicating that it subject to the indeterministic stack size mentioned above.But when you raise the number of nested
synchronized
blocks significantly, the number of nested invocations becomes smaller and stable. On my environment, it produced aStackOverflowError
after 30 nested calls when having 1,000 nestedsynchronized
blocks and 15 nested calls when having 2,000 nestedsynchronized
blocks, which is pretty consistent, indicating that the method invocation overhead has become irrelevant.This implies more than 30,000 acquisitions, roughly half the number achieved with the ASM generated code, which is reasonable considering that the
javac
generated code will ensure a matching number of acquisitions and releases, introducing a synthetic local variable holding the reference of the object that must be released for eachsynchronized
block. This additional variable reduces the available stack size. It’s also the reason why we now see theStackOverflowError
and noIllegalMonitorStateException
, as this code correctly does structured locking.Like with the other example, running with larger stack size raises the reported number, scaling linearly. Extrapolating the results implies that it would need a stack size of several GB to acquire the monitor
Integer.MAX_VALUE
times. Whether there is a limiting counter or not, becomes irrelevant under these circumstances.Of course, these code examples are so far away from real life application code, that it should not be surprising that not much optimizations happened here. For real life application code, lock elimination and lock coarsening may happen with a much higher likelihood. Further, real life code will do actual operations needing stack space on their own, rendering the stack requirements of synchronization negligible, so there’s no practical limit.
Not a direct answer, but since the only way to get a lot of reentries into
synchronized
blocks on the same monitor (or even on different monitors for that matter) is recursive method calls (you cannot programmatically lock it in a tight loop for example) you will run out of call stack space before you hit the limit of the counter that the JVM internally keeps for this.Well, first of all, it is plenty enough... But this will be implemented with a re-entry counter, and these things eventually overflow.