Several common locks in Java concurrent programming
"TLDR: Java's thread safety is mainly reflected in three aspects: atomicity, visibility and orderliness. The thread safety of data can be ensured in three ways: transaction management, locking mechanism and version control."
Thread safety has been a thorny issue since the birth of concurrent programming. Java provides a variety of locking mechanisms to balance security and performance.
Java's thread safety is mainly reflected in three aspects:
-
Atomicity: Only one thread can process shared resources at the same time
-
Visibility: Modifications of shared resources by one thread can be seen by other threads in a timely manner
-
Orderliness: The order of instruction execution conforms to the logical sequence of the program
The thread safety of data can be ensured in three ways:
-
Transaction management: ensuring that a series of operations either all succeed or all fail
-
Lock mechanism: ensure that only one thread can modify shared resources at the same time
-
Version control: Through optimistic locking, the version number is recorded when updating data for concurrency conflict detection.

synchronized
This is the most basic lock in Java and has four levels: no lock, biased lock, lightweight lock and heavyweight lock.
-
Lock-free: In a single-threaded situation, even if the synchronized keyword is used, the JVM will automatically optimize and will not trigger locking and unlocking.
-
Biased lock: for situations where there is no competition for locks in a multi-threaded environment. When a thread acquires the lock, the thread ID is recorded. When the thread accesses the synchronized code block again, the lock operation will not be triggered, which is equivalent to no lock.
-Lightweight lock: When multiple threads start to compete for the same resource, the biased lock is upgraded to a lightweight lock. This is a spin lock. When the thread cannot obtain the lock, it will continue to try instead of blocking. Suitable for situations where competition is not fierce and waiting time is short
- Heavyweight lock: If competition continues to intensify, the waiting time becomes longer, and continuous spinning will waste CPU resources, then it will be upgraded to a heavyweight lock. Threads that cannot acquire the lock will be blocked
ReentrantLock
Synchronized is the most basic lock in Java, while the JUC package provides more feature-rich locks and interfaces, especially in the locks sub-package. One of the important locks is ReentrantLock.
ReentrantLock is a reentrant lock, also known as a recursive lock
ReadWriteLock
Read-write locks are suitable for scenarios where there are far more read operations than write operations. It allows multiple reading threads to access shared resources simultaneously, but only allows one writing thread to perform write operations.
Optimistic locking and pessimistic locking
Pessimistic locking assumes that data is likely to be modified by other threads, so it locks shared resources before accessing them. synchronized and ReentrantLock are both implementations of pessimistic locking.
Optimistic locking assumes that the data is unlikely to be modified by other threads, so the resource is not locked. It modifies data through the atomic operation of compare-replace. Specifically, when updating data, the data versions are checked for consistency. If the versions are the same, it means that the data has not been modified by other threads and the current thread can be updated successfully. If the versions are different, it means that the data has been modified. The modification of the current thread failed and you need to try again.
Optimistic locking is usually implemented using CAS operations, version numbers or timestamps
-
CAS operation: compare expected values and update if they are the same
-
Version number mechanism: similar to CAS, but uses the version number as the expected value
-
timestamp: similar to CAS, but uses timestamp as expected value
Source code example of CAS operation:
public class AtomicInteger extends Number implements java.io.Serializable {
//Storage integer value, volatile ensures visibility
private volatile int value;
//Unsafe is used to implement access to underlying resources
private static final Unsafe unsafe = Unsafe.getUnsafe();
//valueOffset is the offset of value in memory
private static final long valueOffset;
//Get valueOffset through Unsafe
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
//Here is the key, return if Compare is successful, otherwise try again in the for loop
if (compareAndSet(current, next))
return current;
}
}
}
A major problem with optimistic locking is that if an update fails, it can result in long spinning retries. Therefore, you need to carefully evaluate the degree of concurrency competition for shared resources when using them.
Common implementations of optimistic locking include: StampedLock's optimistic read lock and CAS mechanism in the database
Spin lock
When the thread is waiting for the lock resource, it will continue to check whether the lock is available instead of entering the blocking state.
In many scenarios, synchronized resources are locked for a short period of time. Switching thread states for this brief period of time may not be worth the gain because the overhead of suspending and resuming the thread may be greater. Spin locks avoid the overhead of thread context switching and are suitable for scenarios with short waiting times. But excessive spin will waste CPU resources.
-
Spin lock and optimistic lock
Both spin locks and optimistic locks use the CAS mechanism, but each has its own emphasis. Optimistic locking does not lock synchronization resources and only uses CAS when updating. Spin locks lock resources, but do not block when the lock fails, but use CAS to keep trying.
Atomic class
Thread local variables
ThreadLocal is another way to solve thread safety in Java. It is suitable for scenarios where each thread needs its own copy of data and these data do not need to be shared across threads. In other words, multiple threads do not need to modify and share the same data.
[Main Thread (Thread A)] [Thread B (Thread B)]
| |
| |
[ ThreadLocalMap ] [ ThreadLocalMap ]
| |
| |
+-----------------------+ +-----------------------+
| key: ThreadLocal1 | | key: ThreadLocal1 |
| value: 5 | | value: 10 |
+-----------------------+ +-----------------------+
| key: ThreadLocal2 | | key: ThreadLocal2 |
| value: "Hello" | | value: "World" |
+-----------------------+ +-----------------------+
As can be seen from the above figure, each Thread contains its own ThreadLocalMap. When we need to use a copy of a shared resource, such as ThreadLocal1, the corresponding key-value pair will be set in the ThreadLocalMap of the thread.
In short, ThreadLocalMap is responsible for storing the mapping relationship between ThreadLocal variables and their values.
Fair lock and unfair lock
The main difference between these two types of locks is whether they need to be queued when multiple threads compete for the lock.
-
Fair lock: Multiple threads acquire locks in the order of request, such as queuing in a queue, ensuring that the thread that requests first acquires the lock first. Common fair locks include: ReentrantLock (can be configured as a fair lock)
-
Unfair lock: Locks are acquired out of order. If the lock is free, the thread can acquire it directly, regardless of whether other threads are waiting. This method can usually improve performance, but it may cause some threads to be "starved" because they cannot obtain the lock for a long time. Common unfair locks include Synchronized
-
Why unfair locks perform better
In fair lock, if the thread fails to acquire the lock, it will enter the blocking state, that is, switch from the running state to the sleeping state. When woken up, it will switch from sleep state to running state. Each state transition involves switching between kernel mode and user mode.
In unfair locks, the thread uses CAS to try to acquire the lock. If successful, it runs directly, avoiding the process of entering the blocking state and reducing the number of switching between kernel mode and user mode.
Shared locks and exclusive locks
The main difference between these two types of locks is whether multiple threads are allowed to hold the lock at the same time.
The Synchronized and ReentrantLock discussed earlier are exclusive locks, allowing only one thread to access at the same time. The read lock in the read-write lock is a shared lock, allowing multiple read threads to hold the lock at the same time.