Java Concurrency¶
Threads vs CPU¶
Essentially,Threads and CPU have Producer and Consumer relationship. Threads are the producers produce tasks (from instructions). CPUs are the consumer consume (execute) these tasks. Some tasks should wait for other resources (e.g. I/O). If CPUs have no other tasks to do during waiting, CPUs's capacity will be wasted.
Multi Threads aims to produce enough tasks to feed CPU's capacity, to improve CPU's utilization.
JMM Abstraction from the point of view of concurrency¶
JMM is abstract memory model from a different aspect comparing to the real JVM Memory model. It defines the abstract relationship between threads and main memory:
- shared variables between threads are stored in the main memory (Main Memory).
- each thread has a private local memory (Local Memory), and the local memory stores the Threads to read/write copies of shared variables.
- Local memory is an abstraction of JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations.
Race condition happens when 2+ threads process the same shared variable concurrently.
Key concerns of Java Concurrency¶
Visibility, Atomicity, Orderliness
Mutable and Immutable Objects¶
Mutable object (non-thread-safe) – You can change the states and fields after the object is created. For examples: StringBuilder, java.util.Date and etc.
Immutable object (thread-safe) – You cannot change anything after the object is created. For examples: String, boxed primitive objects like Integer, Long and etc.
Happens-before relationship¶
Happens-before relationship is a guarantee that action performed by one thread is visible to another action in different thread. Happens-before defines a partial ordering on all actions within the program.
Synchronized, Volatile vs CAS (Compare and Swap)¶
Synchronized: The synchronized keyword in Java provides pessimistic locking, which ensures mutually exclusive access to the shared resource and prevents data race. It is based on Monitor of system (MONITORENTER, MONITOREXIT
). We can use jvm parameters e.g. -XX:+UseBiasedLocking
to change the default lock type of jvm.
Volatile: Java volatile variable, which will instruct JVM threads to read the value of the volatile variable from main memory and don’t cache it locally. Use case example: Control flag.
CAS: java.util.concurrent.atomic, e.g. AtomicBoolean,AtomicInteger,AtomicLong, etc. Use case example: Counter
It is Optimistic locking:
- if value in memory matches old value in hand, then swap. [Value in Memory, Old Value(in hand), New value]
- if the values do not match it means some thread in between has changed the value. It will recalculate the old value and new value and then go ahead to do step A.
Cons:
1. CPU high cost for spin.
2. can not ensure atomic of one block.
3. ABA Problem (solution -> add timestamp, e.g. AtomicStampedReference
).
HOW-MANY threads are appropriate?¶
It depends on use cases.
Use Case 1: Only CPU calculations
In theory, it could be
Number of Threads == Number of CPUs
Normally, we could set
Number of Threads = Number of CPUs + 1
the additional one as a buffer.
Use Case 2: CPU + I/O Operations
In case of one CPU,
Number of Threads == 1 + (I/O Processing Time / Processing Time of CPU)
In case of multiple CPUs,
Number of Threads == Number of CPU * [ 1 + (I/O Processing Time / Processing Time of CPU) ]
Lessons Learned¶
- Prefer to use Synchronized rather than the Locks(
e.g. ReentrantLock
) in J.U.C. - Threads must be provided by the thread pool. Explicit creation of threads in the application is not allowed.
- Executors are not allowed to create thread pools. Instead, ThreadPoolExecutor should be used. This way of processing allows the coders to be more specific about the running rules of thread pools and avoid the risk of resource exhaustion.