Since Java 5, the java.util.concurrent.locks package contains some lock implementations, so you don't have to implement your own locks anymore. But you still need to understand how to use these locks.
A simple lock
Let's start with a synchronization block in java:
public class Counter{ private int count = 0; public int inc(){ synchronized(this){ return ++count; } }}You can see that there is a synchronized(this) code block in the inc() method. This code block can ensure that only one thread can execute return ++count at the same time. Although the code in the synchronized synchronization block can be more complex, the simple operation of ++count is enough to express the meaning of thread synchronization.
The following Counter class uses Lock instead of synchronized to achieve the same goal:
public class Counter{ private Lock lock = new Lock(); private int count = 0; public int inc(){ lock.lock(); int newCount = ++count; lock.unlock(); return newCount; }}The lock() method locks the Lock instance object, so all threads that call the lock() method on the object will be blocked until the unlock() method of the Lock object is called.
Here is a simple implementation of the Lock class:
public class Counter{public class Lock{ private boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } public synchronized void unlock(){ isLocked = false; notify(); }}Note the while(isLocked) loop inside, which is also called "spin lock". When isLocked is true, the thread calling lock() blocks and waits on the wait() call. To prevent the thread from returning from wait() without receiving the notify() call (also called false wakeup), the thread will recheck the isLocked condition to determine whether it can be safely continued to execute or needs to keep waiting again, instead of thinking that the thread can be safely continued to execute after being awakened. If isLocked is false, the current thread will exit the while(isLocked) loop and set isLocked back to true, so that other threads calling the lock() method can add locks on the Lock instance.
When the thread completes the code in the critical section (located between lock() and unlock()), unlock() is called. Execution of unlock() will re-set isLocked to false and notify (wake up) one of the threads that have called the wait() function in the lock() method and are in a waiting state.
Reentrability of locks
The synchronized synchronization block in Java is reentrant. This means that if a java thread enters the synchronized synchronization block in the code and thus obtains a lock on the pipe corresponding to the synchronization object used by the synchronization block, then the thread can enter another java code block synchronized by the same pipeline object. Here is an example:
public class Reentrant{ public synchronized outer(){ inner(); } public synchronized inner(){ //do something }}Note that both outer() and inner() are declared synchronized, which is equivalent to synchronized (this) blocks in Java. If a thread calls outer(), there is no problem calling inner() in outer(), because both methods (code blocks) are synchronized by the same management object ("this"). If a thread already has a lock on a pipe object, it has access to all code blocks synchronized by the pipe object. This is reentrant. A thread can enter any block of code synchronized by a lock it already has.
The lock implementation given above is not reentrant. If we rewrite the Reentrant class like the following, when the thread calls outer(), it will block at lock.lock() of the inner() method.
public class Reentrant2{ Lock lock = new Lock(); public outer(){ lock.lock(); inner(); lock.unlock(); } public synchronized inner(){ lock.lock(); //do something lock.unlock(); }}The thread calling outer() will first lock the Lock instance and then continue to call inner(). In the inner() method, the thread will try to lock the Lock instance again, and the action will fail (that is, the thread will be blocked), because the Lock instance is already locked in the outer() method.
If unlock() is not called between lock() twice, the second call to lock will block. After seeing the implementation of lock(), you will find that the reason is obvious:
public class Lock{ boolean isLocked = false; public synchronized void lock() throws InterruptedException{ while(isLocked){ wait(); } isLocked = true; } ...}Whether a thread is allowed to exit the lock() method is determined by the conditions in the while loop (spin lock). The current judgment condition is that the lock operation is allowed only when isLocked is false, without considering which thread locks it.
In order to make this Lock class reentrable, we need to make a little change to it:
public class Lock{ boolean isLocked = false; Thread lockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread callingThread = Thread.currentThread(); while(isLocked && lockedBy != callingThread){ wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock(){ if(Thread.currentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } ...}Note that the current while loop (spin lock) also takes into account the thread that has locked the Lock instance. If the current lock object is not locked (isLocked = false), or the current calling thread has locked the Lock instance, then the while loop will not be executed, and the thread calling lock() can exit the method (Translator's note: "Allowed to exit the method" in the current semantics means that it will not call wait() and cause blockage).
In addition, we need to record the number of times the same thread repeatedly locks a lock object. Otherwise, one unblock() call will unblock the entire lock, even if the current lock has been locked many times. We do not want the lock to be unlocked until the unlock() call reaches the number of times the corresponding lock() call is called.
Now this Lock class is reentrant.
The fairness of the lock
Java's synchronized blocks do not guarantee the order in which threads attempt to enter them. Therefore, if multiple threads continue to compete to access the same synchronized synchronization block, there is a risk that one or more threads will never get access - that is, access is always assigned to other threads. This situation is called thread hunger. To avoid this problem, locks need to achieve fairness. The locks shown in this article are internally implemented with synchronized synchronization blocks, so they are not guaranteed to be fair.
Call unlock() in finally statement
If Lock is used to protect the critical area, and the critical area may throw an exception, it is very important to call unlock() in the finally statement. This ensures that the lock object can be unlocked so that other threads can continue to lock it. Here is an example:
lock.lock();try{ //do critical section code, //which may throw exception} finally { lock.unlock();}This simple structure ensures that the Lock object can be unlocked when an exception is thrown in the critical area. If unlock() is not called in the finally statement, when an exception is thrown in the critical section, the Lock object will remain in the locked state forever, which will cause all other threads calling lock() on the Lock object to block.
The above is the information about Java multi-threaded locking. We will continue to add relevant information in the future. Thank you for your support for this site!