I have a class loader LoaderX that inherits from URLClassLoader
. This class loader implements the following methods:
override fun loadClass(name: String, resolve: Boolean): Class<*> {
try {
return super.loadClass(name, resolve)
} catch (_: ClassNotFoundException) { }
return pluginClassLoaderManager.loadClass(name, this)
}
override fun loadClassNoParent(name: String): Class<*> {
val lock = getClassLoadingLock(name)
synchronized(lock) {
return findLoadedClass(name) ?: findClass(name)
}
}
Here, pluginClassLoaderManager
is a class loader manager that manages all the class loaders in the program. Calling its loadClass
method delegates the loading operation to the loadClassNoParent
method of other class loaders.
The implementation of getClassLoadingLock
in URLClassLoader
always returns this
(I have not enabled parallel class loading).
In theory, this should work fine, but when I run the program, I find that this logic can potentially lead to a deadlock:
"DefaultDispatcher-worker-3@2727" tid=0x24 nid=NA waiting for monitor entry
java.lang.Thread.State: BLOCKED
Blocked by DefaultDispatcher-worker-2@2726
Waiting for DefaultDispatcher-worker-2@2726 to release lock <0x157e> (x.PluginUrlClassLoader)
at x.PluginUrlClassLoader.loadClassNoParent(PluginUrlClassLoader.kt:82)
at x.PluginClassLoaderManager.loadClass(PluginClassLoaderManager.kt:73)
at x.PluginUrlClassLoader.loadClass(PluginUrlClassLoader.kt:76)
at java.lang.ClassLoader.loadClass(ClassLoader.java:526)
"DefaultDispatcher-worker-2@2726" tid=0x23 nid=NA waiting for monitor entry
java.lang.Thread.State: BLOCKED
Blocked by DefaultDispatcher-worker-3@2727
Waiting for DefaultDispatcher-worker-3@2727 to release lock <0x157f> (x.PluginUrlClassLoader)
at x.PluginUrlClassLoader.loadClassNoParent(PluginUrlClassLoader.kt:82)
at xr.PluginClassLoaderManager.loadClass(PluginClassLoaderManager.kt:73)
at x.PluginUrlClassLoader.loadClass(PluginUrlClassLoader.kt:76)
at java.lang.ClassLoader.loadClass(ClassLoader.java:526)
The two threads are waiting for each other to release locks, but I can't figure out under what circumstances this conflict would occur. Logically, once a thread acquires a lock
, it should execute its own findClass
logic and then return a Class
(or throw an exception). Why would two threads end up waiting for each other's locks?
I have a class loader LoaderX that inherits from URLClassLoader
. This class loader implements the following methods:
override fun loadClass(name: String, resolve: Boolean): Class<*> {
try {
return super.loadClass(name, resolve)
} catch (_: ClassNotFoundException) { }
return pluginClassLoaderManager.loadClass(name, this)
}
override fun loadClassNoParent(name: String): Class<*> {
val lock = getClassLoadingLock(name)
synchronized(lock) {
return findLoadedClass(name) ?: findClass(name)
}
}
Here, pluginClassLoaderManager
is a class loader manager that manages all the class loaders in the program. Calling its loadClass
method delegates the loading operation to the loadClassNoParent
method of other class loaders.
The implementation of getClassLoadingLock
in URLClassLoader
always returns this
(I have not enabled parallel class loading).
In theory, this should work fine, but when I run the program, I find that this logic can potentially lead to a deadlock:
"DefaultDispatcher-worker-3@2727" tid=0x24 nid=NA waiting for monitor entry
java.lang.Thread.State: BLOCKED
Blocked by DefaultDispatcher-worker-2@2726
Waiting for DefaultDispatcher-worker-2@2726 to release lock <0x157e> (x.PluginUrlClassLoader)
at x.PluginUrlClassLoader.loadClassNoParent(PluginUrlClassLoader.kt:82)
at x.PluginClassLoaderManager.loadClass(PluginClassLoaderManager.kt:73)
at x.PluginUrlClassLoader.loadClass(PluginUrlClassLoader.kt:76)
at java.lang.ClassLoader.loadClass(ClassLoader.java:526)
"DefaultDispatcher-worker-2@2726" tid=0x23 nid=NA waiting for monitor entry
java.lang.Thread.State: BLOCKED
Blocked by DefaultDispatcher-worker-3@2727
Waiting for DefaultDispatcher-worker-3@2727 to release lock <0x157f> (x.PluginUrlClassLoader)
at x.PluginUrlClassLoader.loadClassNoParent(PluginUrlClassLoader.kt:82)
at xr.PluginClassLoaderManager.loadClass(PluginClassLoaderManager.kt:73)
at x.PluginUrlClassLoader.loadClass(PluginUrlClassLoader.kt:76)
at java.lang.ClassLoader.loadClass(ClassLoader.java:526)
The two threads are waiting for each other to release locks, but I can't figure out under what circumstances this conflict would occur. Logically, once a thread acquires a lock
, it should execute its own findClass
logic and then return a Class
(or throw an exception). Why would two threads end up waiting for each other's locks?
1 Answer
Reset to default 0Instead of relying on instance-level locks (getClassLoadingLock
), consider using a centralized locking mechanism for all your custom class loaders. You can implement a global lock map based on class names being loaded
private val lockRegistry = ConcurrentHashMap<String, Any>()
private fun getGlobalLoadingLock(name: String): Any {
return lockRegistryputeIfAbsent(name) { Any() }
}
Then, whenever loadClassNoParent or pluginClassLoaderManager.loadClass is invoked, synchronize on this global lock for the specific class name
override fun loadClassNoParent(name: String): Class<*> {
val lock = getGlobalLoadingLock(name)
synchronized(lock) {
return findLoadedClass(name) ?: findClass(name)
}
}
This ensures that loadClassNoParent for the same class name is never called concurrently across different threads.
getClassLoadingLock(name)
for the entire duration of your currentloadClass
implementation. – Mark Rotteveel Commented yesterday