I was thinking about thread synchronization issues in compiled languages like C++, versus synchronization issues in languages like Java.

I'm wondering how a JVM language like Java doesn't (at least in practice) suffer from coredumps/segfaults/undefined behavior when race conditions occur.

Consider a C++ program with 2 threads. Suppose each thread shares a reference to an std::vector<int>. Each thread continuously loops, and calls std::vector<int>::push_back() - with no synchronization, no locking whatsoever. In this scenario, it's very likely the program will segfault immediately, at least on any mainstream platform/compiler. The reason is obvious: if one thread triggers a reallocation via a call to push_back, and the other thread ends up writing to the old, free'd memory buffer, your program is likely to core dump immediately. Plus, the internal vector size and capacity values may get corrupted, leading to all sorts of undefined behavior and crashing.

But this doesn't seem to happen in Java. Given the same scenario, where you have two threads, each with a reference to some unsynchronized data structure (like an ArrayList<Integer>), and each thread calls ArrayList<Integer>.add(), the worst that ever seems to happen in practice is an exception is thrown, probably an ArrayIndexOutofBounds exception - and of course, the order of insertion is totally random.

I realize the JVM is just executing byte code (except when it's executing JIT'd code), but ultimately that byte code or JIT machine code needs to interact with actual system memory. I assume that Java automatically checks array indices with each access, and the language semantics and garbage collector ensure that all references are non-dangling - but with an unsynchronized data structure, when memory could literally be pulled out from under the program's feet by another thread, at any time, how is the JVM not segfaulting in this scenario unless it's somehow synchronizing memory reads/writes?

up vote 2 down vote accepted

Java implements the Java Memory Model which states what should happen in certain situations, and dumping core is not mentioned anywhere in this document. So any Java implementation must take care to implement what the memory model says, and thus throwing core is not permitted. It does sometimes happen, though rarely, but only as a result of bugs in the implementation.

How a particular implementation achieves adherence to the Memory Model rules is left to the implementor - it is only the final behavior which matters. Others have already mentioned that the Garbage Collector plays an important role here and that in particular as long as you have references to an object in any thread, that object can not be freed from memory.

Consider the effects race conditions can have on actual hardware, not in the C++ undefined behavior theory.

On the high level, you get unpredictable insertion order, possibly lost insertions, or even corrupted data structures. You can totally get these in Java. (If you do racing insertions/removals on a LinkedList for long enough, eventually you'll probably have inconsistent next/prev links on the nodes leading to differing forward and backward traversal sequences.) They do not lead to crashes because they're high level behavior. But they will lead to various exceptions, or simply unexpected data.

On the mid level, you get pointers changed under your feet, indices that are out of date, etc. Java's semantics ensures that these are cleanly caught and converted to exceptions. Null pointers are always checked, indices are always checked, and the garbage collector makes dangling pointers impossible. (The garbage collector is implemented in a fully thread-safe way. No matter what race conditions the programmer writes, he cannot interfere with the garbage collector, because the synchronization there is not visible to the programmer.) These checks are written in a way that prevents race conditions to circumvent them: for example, arrays in Java cannot change in length, and they cannot be replaced or invalidated while you hold a reference (GC again), so if the compiler just creates a guaranteed-thread-local reference before checking the index, there can be no race condition between the index checking and the actual access.

On the low level, you get the really weird stuff: torn reads and writes, out-of-thin-air values, where completely invalid pointer values could appear: the kind of stuff that motivates C++ specifiers to say that any race condition corrupts the program now and forever, and even backwards in time. Java is implemented in a way that at least prevents this really weird stuff. Reads and writes of machine words are aligned, and if the architecture is flaky enough, probably implemented in a safer way than your typical C++ access, at the cost of some performance. By taking this precaution, Java can avoid crashes on the lowest level.

Finally, you have the compiler level, where the compiler will optimize your code under the assumption that race conditions do not happen. This is a nice source of weird errors in C++ even on very consistent architectures such as x64, and a good counterargument to those who claim that "this code is perfectly safe, I know how the CPU works". Java compilers simply don't perform such optimizations where they could lead to crashes. In particular, the special handling of array bounds checking I described above could be optimized away if you assume that race conditions do not happen. Repeated reads from the same pointer, if the pointer is shared but not synchronized, do not need repeated null checks, if you assume that race conditions do not happen. Java compilers do not make this assumption, and either repeatedly check for null, or create an unshared copy of the pointer, which turns the dangerous segfaulting race condition into a "harmless" (doesn't crash, just produces weird behavior) "why does my write have no effect?" race condition.

To sum up, Java does a few low-level things to prevent hard crashes at the cost of a little performance. High-level weird stuff can still happen, but will very often result in some kind of exception, and is usually easier to debug.

In std::vector when a reallocation happens the old array is explicitly deleted, in java.util.ArrayList on the other hand, the old array is left to the garbage collection.

And the GC is conservative enough that the other thread's reference to the array will prevent it from being cleaned up.

but when memory could literally be pulled out from under the program's feet by another thread, at any time, how is Java not segfaulting in this scenario?

When any thread can still reference a block of memory it is not collected. In other words memory is never pulled out of any thread.

  • But one thread might think it can still reference a block of memory, like if you have a linked list, and the "next" pointer is corrupted due to a race condition. I don't see how java would avoid this, unless all memory reads/writes are atomic – Siler Nov 11 '14 at 13:05
  • @Siler Corrupted how? The linked list may not be pointing to the block of memory you're expecting, but it's pointing to a block of memory, which the garbage collector won't collect. – Doval Nov 11 '14 at 13:09
  • But the block of memory it points to might not even be valid - it might be something which was already collected, or some address which is not even inside the program address space. How would the JVM avoid segfaulting? – Siler Nov 11 '14 at 13:11
  • 3
    @Siler No, if there is a (reachable) reference to it, it won't be collected. The GC takes care of that much. – user7043 Nov 11 '14 at 13:20
  • 2
    The linked list will never point to a block of memory. There is no such thing as a "block of memory" in Java. It will either point to an object or a primitive or null. – Jörg W Mittag Nov 11 '14 at 13:21

It's a question of preference. Core dumps can only happen in a (properly implemented) language runtime if you invoke undefined behaviour, since they are never desired; and the designers of Java went to very great length to banish all undefined behaviour from the language.

The attitude of the C++ community tends to be "We need to make it possible to achieve anything anyone might want, for people who know what they are doing; but if someone makes a mistake, that's their problem". Java had many more years of hindsight, and the inventors decided that on the average, people are better served with a language that doesn't allow some things and makes other things somewhat slower than they would optimally be, for the sake of completely banishing runtime problems caused by undefined behaviour. This means checks before array accesses, pointer dereferencing, garbage collection etc., but on the whole the decision paid off, since computers got ever faster and cheaper, while programmers are just as expensive for an employer as they always were.

  • 1
    I'm not asking really about the philosophy behind undefined behavior vs. defined behavior, but rather how it's even technically possible to avoid undefined behavior/core dumps with unsynchronized memory reads/writes – Siler Nov 11 '14 at 13:05
  • 2
    They're not unsynchronized. The Java VM has a well-defined, rigorously implemented memory model that equates to protecting the internal memory pool from the sort of corruption that would lead to an error raised by the OS. You can still mess up your user-defined data structures, but not the ones within the VM. Most other languages use the OS itself as their execution environment, so they are at the mercy of the OS memory model - which is usually less safe. – Kilian Foth Nov 11 '14 at 13:14
  • I see - so the internal memory pool is itself always synchronized via locking or whatever (maybe lock-free algorithms that use atomic primitives)? – Siler Nov 11 '14 at 13:15

@ratch freak's answer is correct but the reason for the segfault is likely to be slightly different.

std::vector holds its values in place. In the case of std::vector<int> that just means that it contains an array of ints. For objects it means it uses inplace new.

So, on a re-allocation of the internal array, any extraneous reference to the original array or the data in it will point to free'd memory so is likely to crash.

In Java, an ArrayList will always hold references to Objects i.e. pointers. So even if the implementation copied its internal array, there is a good chance that the reference you hold will still be in the new array.

In other words, you could make the C++ segfault less often if you used std::vector<int *> . This isn't a recommendation btw. It's now not only a bad idea it's likely to be a slow, bad idea.

@ratchet freak's point is more subtle. Even though the ArrayList doesn't hold any references to its old internal array after the re-allocation, you may do, accidentally. In C++ this will probably barf. In Java, the GC knows you hold a reference, even if you didn't mean to, and won't free the array even though it now no longer belongs to the ArrayList. As a result the behaviour will be unpredictable but it probably won't segfault.

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.