On Mon, 15 Mar 2021 17:15:59 GMT, Chris Hegarty <[email protected]> wrote:
>>> What you have is probably fine, but has any consideration been given to
>>> using the initialization-on-demand holder idiom? e.g.
>>>
>>> ```
>>> static private class CanonicalMapHolder {
>>> static final Map<MethodHandleDesc, Function<DynamicConstantDesc<?>,
>>> ConstantDesc>> CANONICAL_MAP
>>> = Map.ofEntries(..);
>>> }
>>> ```
>>
>> Hello Chris,
>>
>> Although I had thought of some other alternate ways to fix this, this idiom
>> isn't something that I had thought of. Now that you showed this, I thought
>> about it a bit more and it does look a lot more "natural" to read and
>> maintain as compared to the current changes in this PR which does some
>> juggling to avoid this deadlock. However, having thought about your proposed
>> solution, I think _in theory_ it still leaves open the possibility of a
>> deadlock.
>>
>> Here's what the patch looks like with your suggested change:
>>
>> diff --git
>> a/src/java.base/share/classes/java/lang/constant/DynamicConstantDesc.java
>> b/src/java.base/share/classes/java/lang/constant/DynamicConstantDesc.java
>> index 976b09e5665..c7bdcf4ce32 100644
>> --- a/src/java.base/share/classes/java/lang/constant/DynamicConstantDesc.java
>> +++ b/src/java.base/share/classes/java/lang/constant/DynamicConstantDesc.java
>> @@ -64,8 +64,6 @@ public abstract class DynamicConstantDesc<T>
>> private final String constantName;
>> private final ClassDesc constantType;
>>
>> - private static Map<MethodHandleDesc, Function<DynamicConstantDesc<?>,
>> ConstantDesc>> canonicalMap;
>> -
>> /**
>> * Creates a nominal descriptor for a dynamic constant.
>> *
>> @@ -274,25 +272,7 @@ public abstract class DynamicConstantDesc<T>
>> }
>>
>> private ConstantDesc tryCanonicalize() {
>> - var canonDescs = canonicalMap;
>> - if (canonDescs == null) {
>> - canonDescs = Map.ofEntries(
>> - Map.entry(ConstantDescs.BSM_PRIMITIVE_CLASS,
>> DynamicConstantDesc::canonicalizePrimitiveClass),
>> - Map.entry(ConstantDescs.BSM_ENUM_CONSTANT,
>> DynamicConstantDesc::canonicalizeEnum),
>> - Map.entry(ConstantDescs.BSM_NULL_CONSTANT,
>> DynamicConstantDesc::canonicalizeNull),
>> - Map.entry(ConstantDescs.BSM_VARHANDLE_STATIC_FIELD,
>> DynamicConstantDesc::canonicalizeStaticFieldVarHandle),
>> - Map.entry(ConstantDescs.BSM_VARHANDLE_FIELD,
>> DynamicConstantDesc::canonicalizeFieldVarHandle),
>> - Map.entry(ConstantDescs.BSM_VARHANDLE_ARRAY,
>> DynamicConstantDesc::canonicalizeArrayVarHandle));
>> - synchronized (DynamicConstantDesc.class) {
>> - if (canonicalMap == null) {
>> - canonicalMap = canonDescs;
>> - } else {
>> - canonDescs = canonicalMap;
>> - }
>> - }
>> - }
>> -
>> - Function<DynamicConstantDesc<?>, ConstantDesc> f =
>> canonDescs.get(bootstrapMethod);
>> + Function<DynamicConstantDesc<?>, ConstantDesc> f =
>> CanonicalMapHolder.CANONICAL_MAP.get(bootstrapMethod);
>> if (f != null) {
>> try {
>> return f.apply(this);
>> @@ -405,4 +385,15 @@ public abstract class DynamicConstantDesc<T>
>> super(bootstrapMethod, constantName, constantType,
>> bootstrapArgs);
>> }
>> }
>> +
>> + private static final class CanonicalMapHolder {
>> + static final Map<MethodHandleDesc, Function<DynamicConstantDesc<?>,
>> ConstantDesc>> CANONICAL_MAP =
>> + Map.ofEntries(
>> + Map.entry(ConstantDescs.BSM_PRIMITIVE_CLASS,
>> DynamicConstantDesc::canonicalizePrimitiveClass),
>> + Map.entry(ConstantDescs.BSM_ENUM_CONSTANT,
>> DynamicConstantDesc::canonicalizeEnum),
>> + Map.entry(ConstantDescs.BSM_NULL_CONSTANT,
>> DynamicConstantDesc::canonicalizeNull),
>> + Map.entry(ConstantDescs.BSM_VARHANDLE_STATIC_FIELD,
>> DynamicConstantDesc::canonicalizeStaticFieldVarHandle),
>> + Map.entry(ConstantDescs.BSM_VARHANDLE_FIELD,
>> DynamicConstantDesc::canonicalizeFieldVarHandle),
>> + Map.entry(ConstantDescs.BSM_VARHANDLE_ARRAY,
>> DynamicConstantDesc::canonicalizeArrayVarHandle));
>> + }
>> }
>>
>>
>> Please consider the following, where I try and explain the theoretical
>> possibility of a deadlock with this approach:
>>
>> 1. Let's consider 2 threads T1 and T2 doing concurrent execution
>>
>> 2. Let's for a moment assume that neither `DynamicConstantDesc` nor
>> `ConstantDescs` classes have been initialized.
>>
>> 3. T1 does a call to `DynamicConstantDesc.ofCanonical(...)` and T2 does a
>> call to something/anything on `ConstantDescs`, which triggers a class
>> initialization of `ConstantDescs`.
>>
>> 4. T1 (which called `DynamicConstantDesc.ofCanonical(...)`) will reach the
>> `tryCanonicalize` and will access `CanonicalMapHolder.CANONICAL_MAP` in the
>> implementation of that method. Because of this access (and since
>> `CanonicalMapHolder` hasn't yet been initialized), T1 will obtain (an
>> implicit) lock on the `CanonicalMapHolder` class (for the class
>> initialization). Let's assume T1 is granted this lock on
>> `CanonicalMapHolder` class and it goes ahead into the static block of that
>> holder class to do the initialization of `CANONICAL_MAP`. To do so, because
>> of the code in the static block of `CanonicalMapHolder`, it now requires a
>> class initialization lock on `ConstantDescs` (since `ConstantDescs` hasn't
>> yet been initialized). So it requests for that lock on `ConstantDescs` class.
>>
>> 5. Concurrently, let's say T2 has initiated a class initialization of
>> `ConstantDescs`. So T2 is currently holding a class initialization lock on
>> `ConstantDescs`. While the static initialization of `ConstantDescs` is going
>> on, let's assume (in theory), due to the whole lot of code in the static
>> initialization of `ConstantDescs`, it either directly or indirectly ends up
>> calling `DynamicConstantDesc.ofCanonical(...)`. This then means that T2 now
>> enters the `tryCanonicalize` and accesses `CanonicalMapHolder.CANONICAL_MAP`
>> and since `CanonicalMapHolder` hasn't yet been (fully) initialized (remember
>> T1 is still in the static block of `CanonicalMapHolder`), it tries to obtain
>> a class initialization lock on `CanonicalMapHolder` which is currently held
>> by T1. T1 won't let go that lock since it wants the lock on `ConstantDescs`
>> which is currently held by T2. This effectively ends up triggering the
>> deadlock.
>>
>> This deadlock isn't possible with the current approach that this PR has.
>>
>> I want to clarify that, the deadlock that I explain above with your proposed
>> approach is just a theoretical possibility. The `ConstantDescs` doesn't
>> currently have any direct call to `DynamicConstantDesc.ofCanonical` in its
>> static initialization nor can I see or think of any indirect calls to
>> `DynamicConstantDesc.ofCanonical` from its static block. I need input from
>> you whether I should keep aside this theoretic issue and deal with it
>> if/when we run into it (the test case in this PR, IMO, is robust enough to
>> catch that deadlock if/when it happens due to any future code changes in
>> this area). If you and others think that we can ignore this case, then your
>> proposed approach of using this lazy holder for initialization, IMO, is much
>> cleaner and natural to read and I will go ahead and update this PR to use it.
>>
>> For those interested in seeing this potential theoretical deadlock with the
>> lazy holder approach in action, I just created a separate branch here
>> https://github.com/jaikiran/jdk/commits/8263108-demo-deadlock-theory which
>> consistently reproduces the deadlock when the `DynamicConstantDescTest`
>> jtreg test is run. The crucial part is that I introduced the following
>> (dummy) call in `ConstantDescs`:
>>
>> diff --git
>> a/src/java.base/share/classes/java/lang/constant/ConstantDescs.java
>> b/src/java.base/share/classes/java/lang/constant/ConstantDescs.java
>> index 3a000d1bd4a..3f793f5a8b3 100644
>> --- a/src/java.base/share/classes/java/lang/constant/ConstantDescs.java
>> +++ b/src/java.base/share/classes/java/lang/constant/ConstantDescs.java
>> @@ -286,6 +286,13 @@ public final class ConstantDescs {
>> static final DirectMethodHandleDesc MHD_METHODHANDLE_ASTYPE
>> = MethodHandleDesc.ofMethod(Kind.VIRTUAL, CD_MethodHandle,
>> "asType",
>> MethodTypeDesc.of(CD_MethodHandle,
>> CD_MethodType));
>> +
>> + static {
>> + // just a dummy call to DynamicConstantDesc.ofCanonical
>> + DynamicConstantDesc.ofCanonical(ConstantDescs.BSM_ENUM_CONSTANT,
>> "SHOW_REFLECT_FRAMES",
>> + ClassDesc.of("java.lang.StackWalker").nested("Option"), new
>> ConstantDesc[0]);
>> + }
>> +
>>
>>
>> The following thread dump shows the deadlock with that lazy holder approach:
>>
>> "MainThread" #15 prio=5 os_prio=31 cpu=6.26ms elapsed=120.18s
>> tid=0x00007fe0e58df800 nid=0x5e03 waiting on condition [0x000070000d71d000]
>> java.lang.Thread.State: WAITING (parking)
>> at jdk.internal.misc.Unsafe.park(java.base@17-internal/Native Method)
>> - parking to wait for <0x000000070ff65d90> (a
>> java.util.concurrent.FutureTask)
>> at
>> java.util.concurrent.locks.LockSupport.park(java.base@17-internal/LockSupport.java:211)
>> at
>> java.util.concurrent.FutureTask.awaitDone(java.base@17-internal/FutureTask.java:447)
>> at
>> java.util.concurrent.FutureTask.get(java.base@17-internal/FutureTask.java:190)
>> at DynamicConstantDescTest.main(DynamicConstantDescTest.java:80)
>> at
>> jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(java.base@17-internal/Native
>> Method)
>> at
>> jdk.internal.reflect.NativeMethodAccessorImpl.invoke(java.base@17-internal/NativeMethodAccessorImpl.java:78)
>> at
>> jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(java.base@17-internal/DelegatingMethodAccessorImpl.java:43)
>> at
>> java.lang.reflect.Method.invoke(java.base@17-internal/Method.java:566)
>> at
>> com.sun.javatest.regtest.agent.MainWrapper$MainThread.run(MainWrapper.java:127)
>> at java.lang.Thread.run(java.base@17-internal/Thread.java:831)
>>
>> "pool-1-thread-1" #16 prio=5 os_prio=31 cpu=14.32ms elapsed=120.18s
>> tid=0x00007fe0e58dea00 nid=0x9903 waiting on condition [0x000070000d820000]
>> java.lang.Thread.State: WAITING (parking)
>> at jdk.internal.misc.Unsafe.park(java.base@17-internal/Native Method)
>> - parking to wait for <0x000000070ff59d30> (a
>> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
>> at
>> java.util.concurrent.locks.LockSupport.park(java.base@17-internal/LockSupport.java:341)
>> at
>> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@17-internal/AbstractQueuedSynchronizer.java:506)
>> at
>> java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@17-internal/ForkJoinPool.java:3454)
>> at
>> java.util.concurrent.ForkJoinPool.managedBlock(java.base@17-internal/ForkJoinPool.java:3425)
>> at
>> java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@17-internal/AbstractQueuedSynchronizer.java:1623)
>> at
>> java.util.concurrent.LinkedBlockingQueue.take(java.base@17-internal/LinkedBlockingQueue.java:435)
>> at
>> java.util.concurrent.ThreadPoolExecutor.getTask(java.base@17-internal/ThreadPoolExecutor.java:1061)
>> at
>> java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17-internal/ThreadPoolExecutor.java:1121)
>> at
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17-internal/ThreadPoolExecutor.java:635)
>> at java.lang.Thread.run(java.base@17-internal/Thread.java:831)
>>
>> "pool-1-thread-2" #17 prio=5 os_prio=31 cpu=7.88ms elapsed=120.18s
>> tid=0x00007fe0e4012c00 nid=0x6003 in Object.wait() [0x000070000d923000]
>> java.lang.Thread.State: RUNNABLE
>> at
>> java.lang.constant.DynamicConstantDesc.tryCanonicalize(java.base@17-internal/DynamicConstantDesc.java:275)
>> - waiting on the Class initialization monitor for
>> java.lang.constant.DynamicConstantDesc$CanonicalMapHolder
>> at
>> java.lang.constant.DynamicConstantDesc.ofCanonical(java.base@17-internal/DynamicConstantDesc.java:136)
>> at
>> DynamicConstantDescTest$InvokeOfCanonical.call(DynamicConstantDescTest.java:129)
>> at
>> java.util.concurrent.FutureTask.run(java.base@17-internal/FutureTask.java:264)
>> at
>> java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17-internal/ThreadPoolExecutor.java:1135)
>> at
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17-internal/ThreadPoolExecutor.java:635)
>> at java.lang.Thread.run(java.base@17-internal/Thread.java:831)
>>
>> "pool-1-thread-3" #18 prio=5 os_prio=31 cpu=15.43ms elapsed=120.18s
>> tid=0x00007fe0e4070600 nid=0x9803 in Object.wait() [0x000070000da26000]
>> java.lang.Thread.State: RUNNABLE
>> at
>> java.lang.constant.DynamicConstantDesc.tryCanonicalize(java.base@17-internal/DynamicConstantDesc.java:275)
>> - waiting on the Class initialization monitor for
>> java.lang.constant.DynamicConstantDesc$CanonicalMapHolder
>> at
>> java.lang.constant.DynamicConstantDesc.ofCanonical(java.base@17-internal/DynamicConstantDesc.java:136)
>> at
>> java.lang.constant.ConstantDescs.<clinit>(java.base@17-internal/ConstantDescs.java:292)
>> at java.lang.Class.forName0(java.base@17-internal/Native Method)
>> at java.lang.Class.forName(java.base@17-internal/Class.java:375)
>> at DynamicConstantDescTest$Task.call(DynamicConstantDescTest.java:104)
>> at DynamicConstantDescTest$Task.call(DynamicConstantDescTest.java:87)
>> at
>> java.util.concurrent.FutureTask.run(java.base@17-internal/FutureTask.java:264)
>> at
>> java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17-internal/ThreadPoolExecutor.java:1135)
>> at
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17-internal/ThreadPoolExecutor.java:635)
>> at java.lang.Thread.run(java.base@17-internal/Thread.java:831)
>>
>> "pool-1-thread-4" #19 prio=5 os_prio=31 cpu=5.39ms elapsed=120.18s
>> tid=0x00007fe0e4070c00 nid=0x9603 in Object.wait() [0x000070000db29000]
>> java.lang.Thread.State: RUNNABLE
>> at
>> java.lang.constant.DynamicConstantDesc$CanonicalMapHolder.<clinit>(java.base@17-internal/DynamicConstantDesc.java:390)
>> - waiting on the Class initialization monitor for
>> java.lang.constant.ConstantDescs
>> at
>> java.lang.constant.DynamicConstantDesc.tryCanonicalize(java.base@17-internal/DynamicConstantDesc.java:275)
>> at
>> java.lang.constant.DynamicConstantDesc.ofCanonical(java.base@17-internal/DynamicConstantDesc.java:136)
>> at
>> DynamicConstantDescTest$InvokeOfCanonical.call(DynamicConstantDescTest.java:129)
>> at
>> java.util.concurrent.FutureTask.run(java.base@17-internal/FutureTask.java:264)
>> at
>> java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17-internal/ThreadPoolExecutor.java:1135)
>> at
>> java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17-internal/ThreadPoolExecutor.java:635)
>> at java.lang.Thread.run(java.base@17-internal/Thread.java:831)
>
>> If you and others think that we can ignore this case, then your proposed
>> approach of using this lazy holder for initialization, IMO, is much cleaner
>> and natural to read and I will go ahead and update this PR to use it.
>
> For me, at least, the holder pattern is clearer. I'm happy with that
> approach. ( I don't have an objection to the alternative, just a mild
> preference for the holder )
Hello Chris, using the holder pattern sounds fine to me then. I've updated this
PR accordingly. The test continues to pass with this fix.
Peter, I didn't get a chance to try out the `@Stable` for the previous approach
but given that we decided to use this holder pattern, we will no longer need
the performance tweaks.
-------------
PR: https://git.openjdk.java.net/jdk/pull/2893