1

From the library I use I get ConcurrentMap. I want to obtain it's keys as Set.

How to do it in thread-safe way (since Kotlin collections API is thread-unsafe)?

5
  • 4
    The API is not really relevant, it's the implementation that decides whether it's thread safe. ConcurrentMap makes various guarantees. Even if you use it via the Map interface, those guarantees still hold. Commented Nov 2, 2024 at 9:33
  • Maybe the only thing we need to be careful with are operations like getOrPut. They were added by Kotlin and they aren't entirely thread-safe even for ConcurrentMap. Otherwise, it doesn't matter if we use Java or Kotlin. Commented Nov 2, 2024 at 10:03
  • @broot: Are you sure about that? The documentation claims otherwise: "Concurrent getOrPut, that is safe for concurrent maps.", and looking at the implementation it seems to rely on the ConcurrentHashMap's internal putVal method for its thread-safety. Commented Nov 2, 2024 at 10:16
  • @tyg Sure, but we use this implementation only if we type the map specifically as ConcurrentMap. If we store it as just MutableMap, we will use implementation which is not thread-safe. I suspect most people use the base map type. Commented Nov 2, 2024 at 11:04
  • Ah, I see. Yes, that will be an issue when the type is changed. But then again, I wouldn't assume any thread-safety in the first place if I just see a MutableMap. So the behavior wouldn't be that unexpected. Commented Nov 2, 2024 at 11:15

2 Answers 2

1

Guarantees expected upon implementations of ConcurrentMap's implementations according to the docs:

A Map providing thread safety and atomicity guarantees.

To maintain the specified guarantees, default implementations of methods including putIfAbsent(K, V) inherited from Map must be overridden by implementations of this interface. Similarly, implementations of the collections returned by methods Map.keySet(), Map.values(), and Map.entrySet() must override methods such as removeIf when necessary to preserve atomicity guarantees.

Memory consistency effects: As with other concurrent collections, actions in a thread prior to placing an object into a ConcurrentMap as a key or value happen-before actions subsequent to the access or removal of that object from the ConcurrentMap in another thread.

This interface is a member of the Java Collections Framework.

An implementation is ConcurrentHashMap:

A hash table supporting full concurrency of retrievals and high expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

ConcurrentHashMaps support a set of sequential and parallel bulk operations that, unlike most Stream methods, are designed to be safely, and often sensibly, applied even with maps that are being concurrently updated by other threads; for example, when computing a snapshot summary of the values in a shared registry. There are three kinds of operation, each with four forms, accepting functions with keys, values, entries, and (key, value) pairs as arguments and/or return values. Because the elements of a ConcurrentHashMap are not ordered in any particular way, and may be processed in different orders in different parallel executions, the correctness of supplied functions should not depend on any ordering, or on any other objects or values that may transiently change while computation is in progress; and except for forEach actions, should ideally be side-effect-free. Bulk operations on Map.Entry objects do not support method setValue.

Another implementation is ConcurrentSkipListMap:

This class implements a concurrent variant of SkipLists providing expected average log(n) time cost for the containsKey, get, put and remove operations and their variants. Insertion, removal, update, and access operations safely execute concurrently by multiple threads.

I suggest that you should try the scenarios you are worried about with one (or both) of these implementations, see whether they fulfill your expectation and if so, then you're done. If not, then detect which methods do not behave in the way you expect and create a subclass where you fix the issues and use that instead.

Sign up to request clarification or add additional context in comments.

Comments

1

Just access it normally:

val keys: MutableSet<YourKeyType> = getConcurrentMap().keys

You can even remove entries from the map in a thread-safe way using the key set:

keys.remove(someKey)

Just keep in mind that the content of keys can change anytime (because it is mutable), but you can be sure that you won't run into a ConcurrentAccessException under any circumstances.


Explanation:

Kotlin follows another paradigm when it comes to shared mutable state than Java.

Java has had a clunky concurrency handling for the most time, so the easiest way for the user to obtain thread-safety is when the data structures themselves have that built in, like the ConcurrentMap implementations.

Kotlin on the other hand refrains from providing built-in concurrency support for its data structures. The user is supposed to manage the thread safety, which fortunately is pretty easy with coroutines. That also has the benefit that you can manage complex state where multiple data structures are involved the same way as simple state which is more efficient and less error prone to get it wrong.

The reason why using a ConcurrentMap implementation in Kotlin simply works is that the Java implementation already provides the thread safety. Whatever you do with it in Kotlin, it doesn't lose that characteristic. Using the key set to remove an item from the map is thread-safe as well because the ConcurrentMap's implementation made sure to provide a thread-safe set of keys that is linked to the map. So any modification of that set also modifies the map - the thread-safety is built in.

2 Comments

What makes you think Kotlin has a different approach to sharing mutable state than Java? Sharing mutable state is analogous when using Kotlin coroutines as when using Java threads. We have locks/mutexes, etc. Kotlin stdlib is rather limited, because it has to be included at runtime as a library, so its size matters. Also because it is multiplatform, so it wouldn't make sense to provide e.g. concurrent maps for JavaScript, which is single-threaded. And Kotlin provides e.g. Channel which is essentially a coroutine-safe queue.
Everything in your "shared mutable state" link equally applies to Java (except Java doesn't have coroutines, so the use of multiple threads is more explicit).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.