No description
Find a file
2019-09-24 17:00:02 +02:00
src minor textual updates 2019-09-24 17:00:02 +02:00
.gitignore minor update 2019-09-20 13:40:07 +02:00
readme.md minor textual updates 2019-09-24 17:00:02 +02:00

Java Concurrency in Practice

I created this code to complement the book by showing the pitfalls of using threads in java and by showing the correct use of some concurrency primitives since java 1.5. It is no replacement for the book and does not necessarily cover all of its aspects. Some code goes beyond the book for instance when trying to actually prove what is said, for instance the thread-unsafety of java.util.HashMap. The code is presented in 'puzzler' style. So it can contain unsafe code and it's up to the reader to discover that. This readme is added as an explanation.

Chapter 1 Fundamentals

  • UnsafeSequence Shows that concurrent reads will see the same value, resulting in undefined behavior. The code uses the treadsafe CopyOnWriteArrayList instead of printing directly to stdout to prove that the writes/updates are not ordered, not the calls to println(). The code also introduces the use of TestHarness that allows any number of threads to operate on the class under test. Internally it uses a CountdownLatch to assure that the threads al start the exact same time instead of creation time (the threads themselves are created sequentially). This technique is described in JCiP ch. 5.1.1
  • SafeSequence Uses synchronized to guarantee ordered reads/updates.
  • Waiter Object.wait() is tricky. For instance the user must use synchronized but in this case does not lock on the monitor. Other threads can still 'synchronize' on it as shown. The more well known catch is that there can be spurious wakeups, so the user needs some boolean value to make sure a wakeup was valid (not shown here).

Cretans

'All the Cretans are liars' -- Epimenides the Cretan (https://en.wikipedia.org/wiki/Epimenides_paradox)

The motivation for this code is that it's easy to shoot yourself in the foot with threads in java and how to prevent that. I would like to add here that java has evolved over the years while keeping less useful concepts (ie. object.wait(), or volatile), while more modern languages have fewer outdated threading primitives and are therefore safer to use.

  • Cretan A simple class that has mutable state and that has no protection from being shared in a dysfunctional way.
  • CretanAttack Shows how the invariant for Cretan can be violated
  • SynchronizedCretan Shows that all access to shared mutable state needs to be guarded by locks, not just the writes
  • SynchronizedCretan2 All access is now guarded by synchronized so 'WTF' no longer occurs. While this style is safe, it's not always the most scalable approach.
  • SynchronizedCretan3 Shows that you can synchronize on anything, but this code is tricky because it only works because the compiler interns the String literal, so it becomes a reference to the same object.
  • SynchronizedCretan4 Is wrong, because new String() always creates a new object, so it no longer serves as a valid lock.
  • SynchronizedCretan5 Is tricky because synchronized on a method uses this as the lock object. So update() and compare() might not use the same lock object.

Sleepers

This code shows the many ways you historically have to make the thread stop for a while. Should never be necessary ... in theory. It's still very common even in production code. Sometimes you just can't get around it.

  • Sleeper1 Still the most used way, using Thread.sleep() where the argument is the number of milliseconds. It is not theoretical that a developer mistakenly thinks they are seconds.
  • Sleeper2 The more modern remedy where the unit is explicit.
  • Sleeper3a Again using Object.wait().
  • Sleeper3b Synchronized still doesn't synchronize on this
  • Sleeper4 Thread.yield(). Don't use it.

Chapter 2 Thread Safety

Chapter 3 Sharing Objects

  • AttemptToShowReordering The java memory model facilitates modern processors that are free to reorder instructions, if (and only if) reordering would not harm sequential execution. While this is accepted theory, it is surprisingly difficult (if not impossible) to show reordering on Intel processors. The code tries to find a situation where variables b or c are set but a is not.
  • VolatileStatus The only valid use case for volatile is when you have 1 writing thread and n>0 reading threads. This is useful for state changes.
  • Visibility The idea here is that the write to value might occur after the write to ready resulting in printing 0. This is because there is no explicit happens-before relation between lines 27 and 28 and the processor is free to reorder the instructions. Using a memory barrier would establish such a happens before. In java that would mean using volatile, an AtomicReference or synchronized. So far I have not seen it print 0. See https://stackoverflow.com/questions/52648800/how-to-demonstrate-java-instruction-reordering-problems
  • VolatileCretan The volatile clause only guarantees visibility. It is not sufficient for thread safety.
  • NonAtomicDoubleUpdates This is again a failed attempt to show something that you shouldn't do. Doubles and Longs are 8 bytes in size. Writes to memory are atomic for 4 bytes only. Two writes of 4 bytes should result in corrupt values if executed from multiple threads. I guess this could lead to negative values whereas the code should only write positives. #fail
  • Publication Constants are guaranteed to be published safely.

Chapter 4 Composing Objects

  • Ownership and Ownership2 These classes are added to show the concept of ownership which is made 'invisible' in java. The only difference in runtime behavior is the car being garbage collected or not. In rust this code would not pass the borrow checker because a reference in that language must only exist in one place (no borrowing of mutable references), so the compiler knows when to deallocate the object safely.
  • Atomicity The CopyOnWriteArrayList is threadsafe, but non-atomic reads/writes are not. Use compound methods like addIfAbsent

Chapter 5 Building Blocks

  • ShowMeTheValues fails with a ConcurrentModificationException because toString() on a list will iterate over it and in this case it is also being updated.

juc

  • HashMapTest Written by Artem Novikov on stackoverflow. Showing the regular HashMap is not threadsafe is easy. While one thread adds keys to the map continually, another thread checks for the existence of a previously added key/value. Note that IntStream.range is exclusive for the upper bound, so targetKey will never be overwritten in the map.

Chapter 6 Task Execution

Futures

  • TheFuture Shows the correct use of java.util.concurrent.Future. A call to getData() is asynchronous and returns a Future immediately. Calls to Future.get() block until a value is set in another thread.
  • CancelledFuture Shows what cancellation does to the future. Calls to Future.get() fail after cancellation.
  • ExceptionalFuture Shows what happens when the calculation raises an error. The Exception 'hides' until Future.get() is called.
  • CompletionServiceExample The JUC CompletionService combines the use of Futures with a BlockingQueue. As tasks are completed, they are added to the queue, where they can be retrieved with take(), which blocks while no completed Future is available.

Threadpools

  • CachedThreadPool Shows the use of a cached threadpool. It has a potentially unlimited number of threads and reuses them when they are discarded by the user.
  • FixedThreadPoolWebserver Shows the use of a fixed threadpool. It has a limited number of threads and reuses them when they are discarded by the user.
  • SingleThreadExecutor Serves to prove that the queue used is guaranteed FIFO so the numbers are being printed in ascending order.

Chapter 7 Task Cancellation

  • TrickyTaskCancellation Shows the subtle difference between interupted() and isInterrupted.
  • Shutdown and ShutdownNow Show the difference between both methods. There are still a lot of tasks in queue when shutdown runs, and they will still be executed. After shutdownNow() there are less dots indicating earlier stop. The currently running task is being interrupted while sleeping.
  • Finalizing The single Finalizer thread is busy finalizing, only until the JVM exits. If you have resources to be closed outside the process, you have no guarantee that that will actually happen. @Deprecated
  • OnShutdown Shutdown hooks are still a valid way to do things before the JVM exits.

Chapter8 Applying Thread Pools

  • Submissions As long as you submit Runnables to a single workerthread, they end up in the queue and are run in order. But adding Callables and hence Futures can lead to deadlocks if tasks are interdependent. The outcome of the outer task is dependent in the inner task, but the inner cannot run as long as the outer is running. So the intended value is never printed.
  • ThreadLocals I'm open to suggestions here, because I think when an uncaught exception occurs in a task, the thread will be replaced.The code in java.util.concurrent.ThreadPoolExecutor (lines 1142-1160 and 994-1020) does indeed suggest that. But the string 'Value is not set' is never printed, implying the worker thread is never actually replaced. Beware though that this may always happen.
  • BlockingQueue juc.BlockingQueue can be used to pass objects to other threads. The take() method blocks until an object becomes available.
  • SynchronousQueues A juc.SynchronousQueue offers a way to hand over a single object between threads. This is an optimization in that it uses no queue. The receiving thread must always call take() before the value can be handed over. Therefore this code raises the IllegalStateException 'Queue full'
  • ParallelStreams Shows multithreaded task execution using parallel streams. A funny thing is that the main thread is also used as a worker, which makes good sense when you think of it.
  • CustomThreadPoolExecutor Executors can be extended using provided hooks. This code (inspired by listing 8.9) shows how to capture task execution times.

chapter 10 Avoiding Liveness Hazards

  • Bank Proves the point made in listing 10.2 concerning deadlock due to dynamic lock-reordering. The from account and to account will very likely be the same (but reversed) in 2 different threads. Locking on the accounts must always happen in the same order to avoid deadlock. The cure for this is in the book in listing 10.3. This code will at some point stop working and the account (lock) reversal is visible in stdout.