Book: Java Concurrency in Practice
Author: Brian Goetz, et. al.
Chapter 3: Sharing Objects
"Sharing" here means sharing between threads. Before we dive deep, let me reiterate some basic behavior by the JVM.
Basic Behavior
--------------
1. The whole JVM memory is roughly divided between "heap" and "stack".
2. All methods are mere container of code, static, and lifeless.
3. Threads are like living persons that walk the instructions mentioned in methods. I will refer a thread that is executing a method as "walking thread".
4. When threads create method-local objects, they are created on the heap; the references to objects are created on the stack. All primitives are created on the stack.
5. When threads create method-local primitives, they are created on the stack.
6. References and primitives passed as parameter are copies from the source. While in a method, threads use their copies.
If the source is modified by some other thread, there is no guarantee that the changes will be visible immediately to the current thread. We should assume that things will go wrong.
7. When a primitive is written, it is done as an "atomic" operation. This means, the complete content is either fully written or not.
Two exceptions - long and double; both of these are 64-bit primitives. They are written as two 32-bit operations.
8. Class-level primitives, references (and objects, of course) are stored in the heap.
Problems
========
* Stale Data caused by Visibility
The problem depicted by #6 is this - the walking thread is seeing. "stale" data. The reason it is happening is because of lack of "visibility" management.
To solve,
Option 1: Use synchronized.
Option 2: Use volatiles.
* 64-bit primitives are non-atomic
Use volatile to solve.
* Unsafe publication
** "this" escapes the constructor
** Reference escapes through different methods
** Mutable objects
Techniques and their usage
==========================
1. synchronized
Solves visibility problem. It provides mutex locks reading/writing - instance primitives and instance references. Method-locals do not need that; thread-confinement guarantees single-thread access, UNLESS we allow a reference escape.
Allows to create atomic operation.
2. volatile
Solves visibility problem.
Must be used with 64-bit primitives to overcome partial writing problem.
Catch: Each read and write operation are guaranteed to reflect the central data. Operations like x++, or x = x + 4 may corrupt data (e.g. after reading x as 3, another thread may have modified it to 10; the current thread will not see that; and hence, it will set x to 7)
To overcome, use volatile only when one thread writes and more than one thread reads.
3. Immutable object
TBD
4.
Other issues to highlight
=========================
1. Statement reordering
2. Multiple processors executing the same method for the same thread.
Things a developer should make sure to eliminate thread-related problems
==================================================================
Method level
------------
1. Do not worry about objects and primitives created as method-local. Unless you explicitly design for it, do not escape any reference to any method-local.
Constructor level
-----------------
1. Do not allow "this" escape during construction. Common ways it can happen - (a) Allowing "this" to another object (b) Staring a thread.
If you have to do all of these, (a) Make your constructor private, (b) Provide factory method, so that you can wire necessary things are the object is created.
Instance variables
------------------
1. Make all 64-bit primitives volatile to fix partial writing problem, or access them only through synchronized getter/setter.
2. Use volatile only when you can make sure that only one thread can modify volatiles.
3. Atomicity of for 1+ instance variables can be also achieved by putting those variables in another object holder where the reference is volatile. Again, only one thread should assign objects to the reference. ** Check this concept.
Non-related topic
=================
1. How does hashtable works in Java