用锁包装的对象上的 ConcurrentModificationException

问题描述 投票:0回答:1

我把我的应用程序搁置了一晚,早上注意到这样的堆栈跟踪:

java.util.ConcurrentModificationException: null
at java.base/java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:756) ~[na:na]
    at java.base/java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:788) ~[na:na]
    at java.base/java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:786) ~[na:na]
    at tech.inno.dsm.config.MyClass.cleanup(MyClass.kt:150) ~[main/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-6.0.12.jar:6.0.12]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:96) ~[spring-context-6.0.12.jar:6.0.12]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

我试图找到根本原因,但没能找到。所以让我提供我的代码的简化版本:

@Configuration
@EnableConfigurationProperties(MyConnectionProperties::class)
class MyClass(
    private val myProperties: MyConnectionProperties,
) {

    private val logger: KLogger = KotlinLogging.logger {}
    private val myLock = ReentrantLock()
    private val myHolder: MutableMap<String, MyConnectionExtension> = mutableMapOf()
     
    @Scheduled(cron = "...")
    fun cleanup() {
        val connectionsToClose: MutableList<LDAPConnection> = mutableListOf()

        myLock.withLock {
            myHolder.forEach { (userDn, connectionExtension) ->
                val lastTouchTime = max(
                    connectionExtension.connection.lastCommunicationTime,
                    connectionExtension.lastPullingTime,
                )
                val connectionName = connectionExtension.connection.connectionName

                logger.trace {
                    "[cleanConnectionHolder]: connection $connectionName: " +
                        "lastTouchTime = ${Instant.ofEpochMilli(lastTouchTime).atZone(ZoneId.systemDefault())}"
                }

                if (lastTouchTime + ldapConnectionProperties.lifetimeMs < Instant.now().toEpochMilli()) {
                    myHolder.remove(userDn)
                    connectionsToClose.add(connectionExtension.connection)

                    logger.trace { "[cleanConnectionHolder]: Connection $connectionName removed from holder" }
                }
            }
        }

        connectionsToClose.forEach { connection ->
            try {
                connection.close()               
            } catch (e: Exception) {
                logger.warn(e) {"....."}
            }
        }

        logger.trace { "job finished" }
    }
    
    private fun getConnection(userId: String, password: String): LDAPConnection {
        myHolder.withLock {
            val connectionExtension = connectionHolder[userId]
            val connection = connectionExtension?.connection

            return if (connection == null || !connection.isConnected) {
                createConnection(userId, password).also {
                    connectionHolder[userId] = LdapConnectionExtension(
                        connection = it,
                        lastPullingTime = Instant.now().toEpochMilli(),
                    )
                }
            } else {                
                connectionExtension.lastPullingTime = Instant.now().toEpochMilli()
                connection
            }
        }
    }
    ...
}

只有 2 个函数使用

myHolder
,所以我只提供了它们。从我的角度来看,对
myHolder
的访问被锁覆盖,因此即使地图在这里不并发,也不会出现并发问题。

这里出了什么问题?

java kotlin concurrency locking concurrentmodificationexception
1个回答
0
投票

正如评论所说,您正在从地图中

remove
,同时使用
forEach
迭代它。
forEach
旋转迭代器,如果在迭代映射时更改映射,则会抛出 ConcurrentModificationException。

解决方案是将要删除的所有元素记录在一个单独的集合中,然后在 forEach 迭代之后检查这些元素并将它们从地图中删除。

© www.soinside.com 2019 - 2024. All rights reserved.