Welcome to King of Concurrency, this is the 14th article in the series.

In the Gold series, we covered concurrency issues such as deadlocks, live locks, thread hunger, and more. In concurrent programming, all of these problems need to be solved. So, in the Platinum series, we’ll start with concurrency issues and explore the locking capabilities that Java provides and how they solve them.

As the first article in the Platinum series, we’ll start with the Lock interface, because it’s the foundation of locking in Java and the foundation of concurrency.

Understand the basics of locking in Java: the Lock interface

In the Bronze series, we introduced the use of the synchronized keyword to lock methods and code blocks. However, while synchronized is very useful and easy to use, it has limited flexibility in controlling the timing of locking and releasing locks. So, in order to use locks more flexibly and to meet the needs of more scenarios, we need to be able to define locks autonomously. Hence the Lock interface.

The most intuitive way to understand Lock is to find it directly in the concurrency utility class provided by the JDK, as shown in the following figure:

As you can see, the Lock interface provides some capability apis and has some concrete implementations, such as ReentrantLock, ReentrantReadWriteLock, and so on.

1. Lock’s five core capability apis

  • void lock(): Obtains the lock.If the current lock is not available, it is blocked until the lock is released;
  • void lockInterruptibly(): Acquires the lock and allows it to be interrupted.This method is similar tolock()Similar, except that it allows to be interrupted and throw an interrupt exception.
  • boolean tryLock(): Attempts to obtain the lock.Results are returned immediately without being blocked.
  • boolean tryLock(long timeout, TimeUnit timeUnit): Try to acquire the lock and wait for some time. This method is similar totryLock(), but it will wait — yes,If the lock is not acquired within the specified time, it will be abandoned;
  • void unlock(): Release the lock.

2. Common implementations of Lock

In the Java concurrency utility class, the Lock interface has several implementations, such as:

  • ReentrantLock: ReentrantLock;
  • ReentrantReadWriteLock: reentrancy read/write lock.

In addition to the two implementations listed, there are several other implementation classes. The details of these implementations are not needed, but will be covered later. At this stage, you need to understand that Locks are their foundation.

2. Custom Lock

Next, let’s look at how the synchronized version of a Lock can be implemented using a Lock, based on the previous example code.

 public static class WildMonster {
   private boolean isWildMonsterBeenKilled;
   
   public synchronized void killWildMonster(a) {
     String playerName = Thread.currentThread().getName();
     if (isWildMonsterBeenKilled) {
       System.out.println(playerName + "Failed to kill the wild monster...");
       return;
     }
     isWildMonsterBeenKilled = true;
     System.out.println(playerName + "Take the wild one!); }}Copy the code

1. Implement a simple lock

Create the WildMonsterLock class and implement the Lock interface. WildMonsterLock will be the key to replacing synchronized:

// Custom lock
public class WildMonsterLock implements Lock {
    private boolean isLocked = false;

    // Implement the lock method
    public void lock(a) {
        synchronized (this) {
            while (isLocked) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            isLocked = true; }}// implement the unlock method
    public void unlock(a) {
        synchronized (this) {
            isLocked = false;
            this.notify(); }}}Copy the code

To implement the Lock interface, you need to implement all of the above methods. However, to simplify the code for presentation, we removed methods such as tryLock from the WildMonsterLock class.

If you’re not familiar with wait and notify, check out the Bronze series. Note that notify must be the same monitor as WAIT when used.

Create a WildMonster class based on the WildMonsterLock you just defined, and replace synchronized with a WildMonsterLock object in the killWildMonster method.

// Use the custom lock
 public class WildMonster {
   private boolean isWildMonsterBeenKilled;
    // Create a lock object
   private Lock lock = new WildMonsterLock(); 

   public void killWildMonster(a) {
     / / acquiring a lock
     lock.lock(); 
     try {
       String playerName = Thread.currentThread().getName();
       if (isWildMonsterBeenKilled) {
         System.out.println(playerName + "Failed to kill the wild monster...");
         return;
       }
       isWildMonsterBeenKilled = true;
       System.out.println(playerName + "Take the wild one!);
     } finally {
       // After execution, do not forget to release the lock at all costslock.unlock(); }}}Copy the code

The following output is displayed:

Nezha conquers the wild monster! Dian Wei did not kill wild monster failure... The King of Lanling failed to kill the wild monster... Kaiwei beheaded wild monster failure... Process finished with exit code 0Copy the code

As can be seen from the result, only Nezha wins the monster. The other heroes all fail, and the result is as expected. This shows that WildMonsterLock achieves the same effect as synchronized.

There are details, however. With synchronized we don’t have to worry about lock release; the JVM does it for us automatically. However, when using custom locks, be sure to use try… Finally to ensure that the lock must eventually be released, otherwise subsequent threads will be blocked.

2. Implement reentrant locks

In synchronized, the lock is reentrant. Reentrant of locks means that locks can be called repeatedly or recursively by threads. For example, if there are multiple lock methods in a lock object, the thread should be able to access other lock methods at the same time after acquiring the lock and entering any of them without blocking. The prerequisite, of course, is that this method uses the lock of the same object (monitor).

In the following code, methods A and B are synchronous methods, and A calls B. Then, the thread has acquired the lock of the current object when calling A, so the thread can call B directly from A. This is the reentrancy of the lock.


public class WildMonster {
    public synchronized void A(a) {
        B();
    }
    
    public synchronized void B(a) { doSomething... }}Copy the code

So, in order for our custom WildMonsterLock to also support reentrant, we need to make a few code changes.

public class WildMonsterLock implements Lock {
    private boolean isLocked = false;
   
    // Key: add a field to hold the current thread that acquired the lock
    private Thread lockedBy = null;
    // Key: increase the number of lock times
    private int lockedCount = 0;

    public void lock(a) {
        synchronized (this) {
            Thread callingThread = Thread.currentThread();
            // Key: check whether it is the current thread
            while(isLocked && lockedBy ! = callingThread) {try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            isLocked = true; lockedBy = callingThread; lockedCount++; }}public void unlock(a) {
        synchronized (this) {
            // Key: check whether it is the current thread
            if (Thread.currentThread() == this.lockedBy) {
                lockedCount--;
                if (lockedCount == 0) {
                    isLocked = false;
                    this.notify();
                }
            }
        }
    }
}
Copy the code

In the new WildMonsterLock, we added this.lockedBy and lockedCount fields, and added thread determination when locking and unlocking. If the current thread has already acquired the lock while locking, it does not have to enter the wait. When unlocking, only the current thread can unlock it.

The lockedCount field is used to ensure that the number of unlocked times matches the number of locked times. For example, if locked three times, the corresponding number of unlocked times should also be unlocked three times.

3. Focus on the fairness of locks

In the Gold series, we mentioned that threads can starve to death in competition because the competition is not fair. Therefore, when we customize the lock, we should also consider the fairness of the lock.

Third, summary

That’s all for Lock. In this article, we’ve shown that Lock is the basis for all types of locks in Java. It is an interface that provides some capability apis and has a complete implementation. Also, we can customize the logic to implement locks as needed. Therefore, when learning about the various locks in Java, it is best to start with the Lock interface. At the same time, in the process of replacing synchronized, we can also feel that Lock has some advantages that synchronized does not:

  • Synchronized is used for method bodies or code blocks, while Lock can be used flexibly, even across methods;
  • Synchronized has no fairness and can be acquired by any thread and held for a long time, thus potentially starving other threads. Based on the Lock interface, we can implement fair locking, thus avoiding some thread activity problems;
  • Synchronized blocks only when waiting, while Lock provides a tryLock method that can be quickly trial-and-error and set a time limit.
  • Synchronized cannot be interrupted, while Lock provides the lockInterruptibly method to do so.

In addition, when defining a custom lock, consider the fairness of the lock. When using locks, it is necessary to consider the safe release of locks.

The test of the master

  • Based on Lock interface, custom implementation of a Lock.

Extensive reading and references

  • Locks in Java
  • “King concurrent class” outline and update progress overview

About the author

Follow technology 8:30am for updates. Deliver high-quality technical articles, push author quality original articles at 8:30 in the morning, push industry in-depth good articles at 20:30 at night.

If this article has been helpful to you, please like it, follow it, and monitor it as we go from bronze to King.