Welcome to Concurrency King Lessons, the 14th article in a series.

In the Gold series, we looked at concurrency issues such as deadlocks, live locks, thread hunger, and so on. In concurrent programming, these problems undoubtedly need to be addressed. So, in the Platinum series, we’ll take a look at the problems in concurrency and explore the locking capabilities provided by Java and how they solve these problems.

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

1. Understand the basis of locking in Java: the Lock interface

In the Bronze series, we introduced the use of locking methods and blocks of code using the synchronized keyword. However, while synchronized is very useful and easy to use, it has very limited flexibility in terms of when locks are added and released. Therefore, 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 figure below:

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

1. Lock’s five core competency APIs

  • void lock(): Get the lock.If the lock is not currently available, it is blocked until the lock is released;
  • void lockInterruptibly(): Gets the lock and allows it to be interrupted.This method is the same aslock()Similar, except that it allows to be interrupted and throw an interrupt exception.
  • boolean tryLock(): Attempts to acquire the lock.The result is returned immediately without being blocked.
  • boolean tryLock(long timeout, TimeUnit timeUnit): Attempts to acquire the lock and waits for some time. This method is the same astryLock(), but it will wait depending on the argument — 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 classes, there are implementations of the Lock interface, such as:

  • ReentrantLock: Reentrant lock;
  • ReentrantReadWriteLock: Reentrant read-write lock;

In addition to the two implementations listed, there are several other implementation classes. I don’t need to go into details about these implementations, but they will be covered later. At this stage, what you need to understand is that Lock is their foundation.

2. Customize LOCK

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

public static class WildMonster { private boolean isWildMonsterBeenKilled; public synchronized void killWildMonster() { String playerName = Thread.currentThread().getName(); If (iswildmonsterBeenKilled) {system.out. println(playerName + "failed to kill a wild monster..." ); return; } isWildMonsterBeenKilled = true; Println (playerName + "Get the monster!" ); }}

1. Implement a simple lock

Create a class called WildmonsterLock and implement the Lock interface. WildmonsterLock will be the key to replacing synchronized:

Public class wildmonsterLock implements Lock {private Boolean isLocked = false; Synchronized (this) {while (isLocked) {try {wait(); } catch (InterruptedException e) { e.printStackTrace(); } } isLocked = true; Public void unlock() {synchronized (this) {isLocked = false; this.notify(); }}}

When implementing the Lock interface, you need to implement all of its methods above. However, to simplify the code and make it easier to show, we have removed methods such as TryLock from the WildMonsterLock class.

If you’re not familiar with wait and notify, check out this article in the Bronze series. Note that notify needs to be the same monitor as wait when used.

Based on the WildMonsterLock you just defined, create the WildMonster class and use the WildMonsterLock object in the killWildMonster method, replacing synchronized.

Public static class WildMonster {private Boolean iswildmonsterBeenKilled; Private Lock Lock = new WildMonsterLock(); Public void killWildMonster() {public void killWildMonster(); try { String playerName = Thread.currentThread().getName(); If (iswildmonsterBeenKilled) {system.out. println(playerName + "failed to kill a wild monster..." ); return; } isWildMonsterBeenKilled = true; Println (playerName + "Get the monster!" ); } finally {// Do not forget to release the lock. Unlock (); }}}

The output is as follows:

Ne Zha wins the monster! Dian Wei did not kill wild strange failure... King Lanling failed to kill wild monsters... Armour failed to kill the wild monster... Process finished with exit code 0

From the results, we can see that only Nezha won the monster, while the other heroes all failed, and the result was as expected. This shows that Wildmonsterlock achieves the same effect as synchronized.

However, there are details to be noted. We don’t have to worry about releasing locks when using synchronized; the JVM does this automatically. However, when using custom locks, be sure to use try… Finally ensures that the lock must eventually be released, otherwise subsequent threads will be severely blocked.

2. Implement reentrant locks

In synchronized, locks are reentrant. Reentrant of a lock means that the lock can be called repeatedly or recursively by a thread. For example, if there are multiple locking methods in a locked object, a thread that acquires the lock and enters one of the methods should be able to enter the other locking methods without being blocked. This assumes, of course, that the locking method uses the lock on the same object (the monitor).

In the following code, methods A and B are synchronized, and A calls B. Then, if the thread has already acquired the lock on the current object when it calls A, the thread can call B directly when it calls A. This is the reentrancy of the lock.

public class WildMonster { public synchronized void A() { B(); } public synchronized void B() { doSomething... }}

So, in order for our custom WildMonsterLock to support reentrancy, we need to make a small change to the code.

public class WildMonsterLock implements Lock { private boolean isLocked = false; Private Thread lockedBy = null; private Thread lockedBy = null; Private int lockedCount = 0; private int lockedCount = 0; public void lock() { synchronized (this) { Thread callingThread = Thread.currentThread(); // while (isLocked && lockedBy! = callingThread) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } isLocked = true; lockedBy = callingThread; lockedCount++; } public void Unlock () {synchronized (this) { If (Thread.currentThread() == this.lockedBy) {lockedCount--; if (lockedCount == 0) { isLocked = false; this.notify(); } } } } }

In the new WildMonsterLock, we added the this.lockedBy and lockedCount fields, and added thread determination when locking and unlocking. When locking, if the current thread has already acquired the lock, it does not have to wait. When unlocked, only the current thread is unlocked.

The lockedCount field is used to ensure that the number of times unlocked matches the number of times unlocked. For example, if the number of times unlocked is 3, the lockedCount field is also 3 times unlocked.

3. Focus on lock fairness

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

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 of implementing locks as needed. So, when learning about locks in Java, it’s a good idea 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 the body of a method or block of code, while Lock can be used flexibly, even across methods;
  • Synchronized is not fair and can be acquired by any thread and held for long periods of time, potentially starving other threads. Based on the Lock interface, we can realize fair locking, thus avoiding some problems of thread activity.
  • Synchronized can only wait when blocked, while Lock provides the TryLock method, which allows for quick trial and error and a time limit that makes it more flexible to use.
  • Synchronized cannot be interrupted, while Lock provides the LockInterruptibly method, which enables interruptions.

In addition, lock fairness should be considered when customizing locks. When using locks, you need to consider the safe release of locks.

Teacher’s trial

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

Extended reading and references

  • Locks in Java
  • “Concurrent king lesson” outline and update progress overview

About the author

Pay attention to the public number [mediocre technology joke], access to timely article updates. Record the technical stories of ordinary people, share quality (as much as possible) technical articles, and occasionally talk about life and ideals. No anxiety peddling, no headline peddling.

If this article is helpful to you, welcome thumb up, follow, monitor, we together from bronze to king.