Search code examples
javaconcurrencyone-liner

Thread safety of single line in Java


Since I'm implementing for the first time a quite complex multi-threaded application in java, I was wondering about thread safety on data structures. Obviously my first goal is to achieve thread safety but I would like also to achieve a good performance avoiding useless extra locks. The hypothesis is that I'm not using concurrent/thread-safe alter ego of the used DSs (I'm going to use DS as "data structure" from now on).

My general approach is this one:

When the same data structure is used for more than one line or there's an iteration on it in the same method/code block I'm going to synchronize that block on the DS. Even if it's a read only block. For example this code:

synchronized (networkView) {
    for (int othersID : networkView.keySet()) {
        networkView.get(othersID).send(new CloseConnectionMessage(ID));
        networkView.get(othersID).close();
        networkView.get(othersID).join();
    }
}

Where networkView is a classic HashMap. This is because while I'm working (iterating/reading multiple times) on that DS I don't want anyone to write it externally. Now every time I'm writing networkView I'm going to synchronize that block even if it's a one-liner to avoid concurrent modification like this other example:

public void clearNetwork() {
    synchronized (networkView) {
        networkView.clear();
    }
}

And here's the first question: is it also necessary to synchronize on one-liners that only read the structure? For example if a method returns the lenght of this data structure, is it thread safe without a synchronized block? like this one:

public int getNetworkView() {
    return networkView.size();
}

So the underlying question is: single line of codes are thread safe or not? As I said before modification one-liners should anyway be synchronized because they should not interfere with mutliple read blocks (like the first example) but can they also interfere with one-liner reads and write? If example 2 and 3 methods are executed concurrently, is it thread safe?

Another question is this one: with DS used only with one-liners I would say it's enough to use their thread-safe counterparts (like ConcurrentHashMap), while they're not enough in a case like the first example without the synchronized block. Am I right about this?


Solution

  • So the underlying question is: single line of codes are thread safe or not?

    That really isn't how it works. Interactions are 'safe' or 'unsafe'; single lines are not individually so.

    However, in general the rule: It's never safe.

    If you fail to synchronize on a .size() call, that may return a size that is wildly wrong. Here's the simple rule:

    • If a field, of any object, is being accessed by 2 or more threads, at least one of which is writing it, that code is broken.

    The only way it isn't broken is if you take care to write a so-called Happens-Before relationship across all such accesses. An HB relationship is a thing described in the Java Memory Model documentation that establishes the following notion:

    • If line A has a happens-before relationship relative to line B, then when B executes, it is not possible to observe state as it was before A finished execution.

    That's all you get.

    Thus, if you fail to establish HB, The JVM is free to feed you state that has long since passed on. However, the JVM is not obligated either - the JMM is the one place in the java documentation where the term 'may' shows up a ton. That's because java has to run on many, many platforms and they just don't behave the same in regards to on-core caches and other such hardware concerns. In order to ensure a JVM can run code speedily instead of having to slow everything down to a crawl with e.g. global interpreter locks the way python and co have to do it, the JMM instead claims sizable swaths of "the JVM is free to do this thing, or not do this thing - it is on you, the coder, to ensure this 'I may or may not do that thing' does not affect the output of your program".

    To establish HB, there are a few strategies:

    • "Natural": Within one thread, obvious lexical rules. Given: foo(); bar(); in a single method, foo() is HB relative to bar(). This one is the obvious one.
    • Thread starting. thread.start(); is HB relative to the first line that thread's run().
    • synchronized. The line exiting a sync block is HB relative to any entering of a sync block, so long as these sync blocks are syncing on the same object of course.
    • access to a volatile field, but note that it tends to be difficult to know which line was HB and which one is HA, with volatilez.

    There are a few more, but those are the basics.

    Thus, here you want the last time this object is modified by some thread to be HB relative to invoking size(), because otherwise size() might as well be telling you what the size() was 18 hours ago.