Multi-threads
Resources:
http://tutorials.jenkov.com/java-concurrency/
http://tutorials.jenkov.com/java-util-concurrent/index.html
Basics
Thread safe: a method or class instance can be used by multiple threads at the same time without any problem.
Immutability: immutable objects are thread safe but the use of it is not thread safe.
Race condition: several threads are reading and writing the same critical region.
synchronized: for synchronization to work, we need a special, technical object that will hold the key. This key is also called a monitor. Synchronized static method, the object is the class itself. (all the classes in Java are represented by Object too)
We can use a dedicated object to synchronize too.
- Gold rule: It is always a good idea to hide an object used for synchronization. (If it is exposed, some other methods e.g. join() might call the wait() and release the key held.)
- Don't call wait() on global objects or constant String's (because the JVM/Compiler internally translates constant strings into the same object.)
public class Person {
private final Object key = new Object();
public String init() {
synchronized(key) {
}
}
}
Thread.interrupted() not only returns the flag but also sets it to false. Thus, once InterruptedException is thrown, the flag is reset. The thread no longer knows anything about the interruption request sent by the owner.
(stop() is legacy for backward compatibility only, shouldn't be used.)
try {
Thread.sleep(100); //sleep method will check .isInterrupted() from time to time
} catch (InterruptedException ex) {
//The entire thread received an interruption request, and we merely swallow it and convert it into a RuntimeException.
//We can't treat such a serious situation so loosely. So, we're setting the flag back to true!
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
wait() / notify(): they are methods for every java objects. The thread executing the invocation should hold the key of the object, otherwise Exception will be raised. The methods cannot be invoked outside a synchronized block.
wait(): release the key and put the thread into Waiting state (thread state). (thus different from the state that the thread is waiting at the entrance of the synchronized block -- Blocked state). But notify() / notifyAll() are the only way to release a waiting thread.
static class Producer {
void produce() {
//wait()/ notify() have to be inside the synchronized block
// because the thread who wait() needs to acquire the lock first.
// Otherwise, java.lang.IllegalMonitorStateException
synchronized(lock) { //the lock object is common one both Producer and Consumer use
if(isFull(buffer)) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer[count++] = 1;
lock.notifyAll();
}
}
}
static class Consumer {
void consume() {
synchronized (lock) {
if(isEmpty()) {
try{
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer[--count] = 0;
lock.notifyAll();
}
}
}
Java Memory Model
- static class variables are also stored on the heap along with the class definition.
Cache, Visibility, Volatile
Visibility: "a read should return the value set by the last write." All the synchronized writes are visible. That means caches in other cores are informed about the modification.
Volatile: Declaring a variable volatile thus guarantees the visibility for other threads of writes to that variable. (read/write from/to main memory.)
http://tutorials.jenkov.com/java-concurrency/volatile.html
- "happens before guarantee". The happens before guarantee guarantees that read and write instructions of volatile variables cannot be reordered. Instructions before and after can be reordered, but the volatile read/write instruction cannot be reordered with any instruction occurring before or after it.
- if two threads are both reading and writing to a shared variable, then using the volatile keyword for that is not enough. You need to use a synchronized in that case to guarantee that the reading and writing of the variable is atomic. (read-update-write is not atomic even the variable is volatile.) (For example: myVar++ is read-update-write, which is not atomic)
- reading or writing a volatile variable does not block threads reading or writing.
- volatile only synchronizes the value of one variable between thread and main memory, while synchronized block synchronizes the value of all variables.
Performance consideration:
- access to main memory is more expensive.
- prevent instruction reordering which is a normal performance enhancement technique.
*Cache Coherence protocol of hardware is trying to address this problem but we cannot rely on hardware.???
For example, below code might be buggy in multicore environment. The getValue is neither synchronized or volatile(l). There is no gurantee the getValue returns the last write value.
public long getValue() {
return l;
}
public void incrementValue() {
synchronized (key) {
l = l + 1;
}
}
Example to use volatile (non-blocking):
You have just one thread writing the variable, all other threads are reading it only. Then no read-update-write race condition.
False Sharing: Cache line/block is 64 bytes. When a thread updates variable a in the cache line, it will mark the cache line dirty and broadcast to other caches of the CPU cores. When other threads want to access a nearby variable in the same cache line, it will encounter cache miss, and reload from main memory.
Java 8 quietly introduced another way of achieving field padding by way of JEP 142 and the _@Contended _annotation which lets us do away with declaring dead variables and instead instructs the JVM to add 128 bytes worth of space before and after the ‘hot’ field.
Double Check Locking (Avoid!!!!)
Due to the semantics of some programming languages, the code generated by the compiler is allowed to update the shared variable to point to a partially constructed object before A has finished performing the initialization. For example, in Java if a call to a constructor has been inlined then the shared variable may immediately be updated once the storage has been allocated but before the inlined constructor initializes the object.[6]
Thread B notices that the shared variable has been initialized (or so it appears), and returns its value. Because thread B believes the value is already initialized, it does not acquire the lock. If B uses the object before all of the initialization done by A is seen by B (either because A has not finished initializing it or because some of the initialized values in the object have not yet percolated to the memory B uses (cache coherence)), the program will likely crash.// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
private Helper helper;
public Helper getHelper() {
if (helper == null) {
synchronized(this) {
if (helper == null) {
helper = new Helper();
}
}
}
return helper;
}
// other functions and members...
}
//Notice the private inner static class that contains the instance of the singleton class.
//When the singleton class is loaded, SingletonHelper class is not loaded into memory
//and only when someone calls the getInstance method, this class gets loaded and creates the Singleton class instance.
//Calling getInstance() references the inner class, triggering the JVM to load & initialize it.
//This is thread-safe, since classloading uses locks.
//For subsequent calls, the JVM resolves our already-loaded inner class & returns the existing singleton. Thus — a cache.
//And thanks to the magic of JVM optimizations, a very very efficient one.
class SingletonInnerStatic {
private SingletonInnerStatic(){}
private static class SingletonHelper {
private static final SingletonInnerStatic INSTANCE = new SingletonInnerStatic();
}
public static SingletonInnerStatic getInstance(){
return SingletonHelper.INSTANCE;
}
}
Starvation and Fairness
- synchronized block no guarantee about the sequence which threads waiting to enter the synchronized block.
- notify() method no guarantee about which thread is awakened.
- FairLock can be implemented by parking waiting threads in a queue with an Object which the thread wait() on.
Compare and Swap
Modern CPUs have built-in support for atomic compare and swap operations.
Compare and swap compares an expected value to the concrete value of a variable, and if the concrete value of the variable is equals to the expected value, swaps the value of the variable for a new variable.
atomic without synchronization (leveraging Compare and Set support from CPU)
Retrying is normal when using atomic operations, and should be expected. Retrying can heavy load on both CPU and memory.
It is called "Optimistic Locking", assume that no other thread will have made changes to the shared memory in the meantime. It tends to work best with low to medium contention on the shared memory. If the content is very high, threads will waste a lot of CPU cycles copying and modifying the shared memory only to fail writing the changes back. (but if you have a lot of content on shared memory, you should always consider redesigning your code to lower the contention.)
private static AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.decrementAndGet();
Common Multithreads Problem
- Nested Monitor Lockout
- Slipped Conditions
- Missing Signals
- Reentrant Lockout
Deadlock
JVM is able to detect deadlock situations, and can log information to help debug the application.
Detect deadlock by Resource Allocation Graph:
- which resource is held by which process
- which process is waiting for a resource of a particular type
Deadlock prevention:
- Lock ordering is a simple yet effective deadlock prevention mechanism. However, it can only be used if yo know about all locks needed ahead of taking any of the locks. This is not always the case.
- Put a timeout on lock attempts. If a thread does not succeed in taking all necessary locks within the given timeout, it will backup, free all locks taken, wait for a random amout of time and then retry.
Spurious Wakeups
To guard against spurious wakeups the signal member variable is checked inside a while loop instead of inside an if-statement. Such a while loop is also called a spin lock. The thread awakened spins around until the condition in the spin lock (while loop) becomes false.
ThreadLocal
Using ExecutorService cannot leverage ThreadLocal as the threads are pooled.
InheritableThreadLocal: grants access to values to a thread and all child threads created by the thread. Use case: when the per-thread-attribute being maintained in the variable (e.g. User ID, Transaction ID) must be automatically transmitted to any child threads that are created.
public static class MyRunnable implements Runnable {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
@Override
public void run() {
threadLocal.set( (int) (Math.random() * 100D) );
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
}
}