Kotlin - How to lock a collection when accessing it from two threads
Asked Answered
E

2

7

wondered if anyone could assist, I'm trying to understand the correct way to access a collection in Kotlin with two threads.

The code below simulates a problem I'm having in a live system. One thread iterates over the collection but another thread can remove elements in that array.

I have tried adding @synchronized to the collections getter but that still gives me a concurrentmodification exception.

Can anyone let me know what the correct way of doing this would be?

class ListTest() {

    val myList = mutableListOf<String>()
        @Synchronized
        get() = field

    init {
        repeat(10000) {
            myList.add("stuff: $it")
        }
    }
}

fun main() = runBlocking<Unit> {

    val listTest = ListTest()

    launch(Dispatchers.Default) {
        delay(1L)
        listTest.myList.remove("stuff: 54")
    }

    launch {
        listTest.myList.forEach { println(it) }
    }
}
Electrolyse answered 9/2, 2020 at 14:33 Comment(2)
You should avoid having shared mutable state like this. What problem are you actually trying to solve? Maybe an «actor» is what you are looking for?Desmond
Even if you synchronize access to the list, the iterator that is created implicitly by forEach is invalidated after a call to remove.Taliped
E
11

You are only synchronizing the getter and setter, so when you start using the reference you get to the list, it is already unlocked.

Kotlin has the Mutex class available for locking manipulation of a shared mutable object. Mutex is nicer than Java's synchronized because it suspends instead of blocking the coroutine thread.

Your example would be poor design in the real world because your class publicly exposes a mutable list. But going along with making it at least safe to modify the list:

class ListTest() {

    private val myListMutex = Mutex()

    private val myList = mutableListOf<String>()

    init {
        repeat(10000) {
            myList.add("stuff: $it")
        }
    }

    suspend fun modifyMyList(block: MutableList<String>.() -> Unit) {
        myListMutex.withLock { myList.block() }
    }
}

fun main() = runBlocking<Unit> {

    val listTest = ListTest()

    launch(Dispatchers.Default) {
        delay(1L)
        listTest.modifyMyList { it.remove("stuff: 54") }
    }

    launch {
        listTest.modifyMyList { it.forEach { println(it) } }
    }
}

If you are not working with coroutines, instead of a Mutex(), you can use an Any and instead of withLock use synchronized (myListLock) {} just like you would in Java to prevent code from within the synchronized blocks from running at the same time.

Earp answered 9/2, 2020 at 15:2 Comment(0)
M
2

If you want to lock a collection, or any object for concurrent access, you can use the almost same construct as java's synchronized keyword.

So while accessing such an object you would do

fun someFun() {
    synchronized(yourCollection) {

    }
}

You can also use synchronizedCollection method from java's Collections class, but this only makes single method access thread safe, if you have to iterate over the collection, you will still have to manually handle the synchronization.

Moskowitz answered 9/2, 2020 at 14:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.