This article focuses on common locks in Java.

preface

The concurrent programming series should be drawing to a close, and locking is probably the last in the series, so the important basics should be covered. Then for the book “Java Concurrent programming Combat”, the last few chapters, I only read the lock part, this article is mainly a simple summary of the lock content in the book.

A deadlock

A deadlock is when a group of threads competing for resources are blocked “permanently” by waiting for each other.

Lock order deadlock

Let’s look at a deadlock example. We’ll define a BankAccount object to store basic information as follows:

public class BankAccount {
    private int id;
    private double balance;
    private String password;
    public int getId(a) {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public double getBalance(a) {
        return balance;
    }
    public void setBalance(double balance) {
        this.balance = balance; }}Copy the code

Next, we use fine-grained locks to try to complete the transfer operation:

public class BankTransferDemo {
    public void transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount) {
        synchronized(sourceAccount) {
            synchronized(targetAccount) {
                if (sourceAccount.getBalance() > amount) {
                    System.out.println("Start transfer.");
                    sourceAccount.setBalance(sourceAccount.getBalance() - amount);
                    targetAccount.setBalance(targetAccount.getBalance() + amount);
                }
            }
        }
    }
}
Copy the code

A deadlock occurs if the following call is made:

transfer(myAccount, yourAccount, 10);
transfer(yourAccount, myAccount, 10);
Copy the code

If the order of execution is out of order, then A may acquire the myAccount lock and wait for yourAccount lock, while B holds yourAccount lock and is waiting for myAccount lock.

Avoid deadlocks by ordering

Since we have no control over the order of the arguments, if we want to solve this problem, we must define the order of the locks and acquire them in this order throughout the application. We can define the lock order using the value returned by Object.hashCode:

public class BankTransferDemo {

    private static final Object tieLock = new Object();

    public void transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount) {

        int sourceHash = System.identityHashCode(sourceAccount);
        int targetHash = System.identityHashCode(targetAccount);

        if (sourceHash < targetHash) {
            synchronized(sourceAccount) {
                synchronized(targetAccount) {
                    if(sourceAccount.getBalance() > amount) { sourceAccount.setBalance(sourceAccount.getBalance() - amount); targetAccount.setBalance(targetAccount.getBalance() + amount); }}}}else if (sourceHash > targetHash) {
            synchronized(targetAccount) {
                synchronized(sourceAccount) {
                    if(sourceAccount.getBalance() > amount) { sourceAccount.setBalance(sourceAccount.getBalance() - amount); targetAccount.setBalance(targetAccount.getBalance() + amount); }}}}else {
            synchronized (tieLock) {
                synchronized(targetAccount) {
                    synchronized(sourceAccount) {
                        if (sourceAccount.getBalance() > amount) {
                            sourceAccount.setBalance(sourceAccount.getBalance() - amount);
                            targetAccount.setBalance(targetAccount.getBalance() + amount);
                        }
                    }
                }
            }
        }
    }
}
Copy the code

No matter how you change the hash value, we always lock the data with the low hash value first and then lock the data with the high hash value, so that the lock order is guaranteed.

However, in rare cases where two objects have the same Hash value, a deadlock can still occur if the order is wrong, so before acquiring both locks, use a tie-breaking lock to ensure that only one thread at a time acquires the lock in an unknown order. However, if the program frequently Hash conflicts, this can become a concurrency bottleneck because final variables are visible in memory, causing all threads to block on the lock, although this is unlikely.

A deadlock occurred between collaborating objects

A. action_a1 () will call action_B1() in B, and b. action_b2 () will call action_A2() in A. As these four methods action_A1(), action_A2(), action_B1() and action_B2() are locked by synchronized, we know that object locks are added to methods by synchronized, so there may be A method called B by A, B is also calling A’s method, resulting in A deadlock between waits.

For specific examples, please refer to page 174 of the Book Java Concurrent Programming In Action.

ReentrantLock

Method of use

While the only mechanisms available to coordinate access to objects are synchronized and volatile, Java 5.0 adds a new mechanism: ReentrantLock. ReentrantLock is not intended as an alternative to built-in locking, but as an optional advanced feature when the built-in locking mechanism is not available.

Here’s a simple example:

Lock lock = new ReentrantLock();
/ /...
lock.lock();
try {
    // ...
} finally {
    lock.unlock();
}
Copy the code

In addition to the above reasons that synchronized is not replaceable, the lock must be released manually through lock.unlock(), which can be a serious problem if forgotten.

Avoid sequential deadlocks by tryLock

Using the deadlock example above, let’s make a simple change with tryLock() :

public boolean transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount, long timeout, TimeUnit unit) {
    long stopTime = System.nanoTime() + unit.toNanos(timeout);
    while (true) {
        if (sourceAccount.lock.tryLock()) {
            try {
                if (targetAccount.lock.tryLock()) {
                    try {
                        if(sourceAccount.getBalance() > amount) { sourceAccount.setBalance(sourceAccount.getBalance() - amount); targetAccount.setBalance(targetAccount.getBalance() + amount); }}finally{ targetAccount.lock.unlock(); }}}finally{ sourceAccount.lock.unlock(); }}if (System.nanoTime() < stopTime) {
            return false;
        }
        // Sleep for a while...}}Copy the code

We first try to acquire the lock of sourceAccount, and then try to acquire the lock of targetAccount if the lock is successful. If the lock fails, we release the lock of sourceAccount to avoid the deadlock problem caused by holding the lock of sourceAccount for a long time.

Locking with a time limit

We can also specify a timeout for tryLock(). If the wait times out, we will not wait forever and execute the subsequent logic directly:

long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
    long nanosToLock = unit.toNanos(timeout);
    if (sourceAccount.lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)) {
        try {
            / / to omit...
        } finally{ sourceAccount.lock.unlock(); }}if (System.nanoTime() < stopTime) {
        return false;
    }
    // Sleep for a while...
}
Copy the code

synchronized vs ReentrantLock

ReentrantLock provides the same semantics for locking and memory as built-in locking, and it also provides several additional features, including timed lock wait, breakable lock wait, fairness, and non-block locking. ReentrantLock seems to perform better than built-in locks, slightly better in Java 6.0 and far better in Java 5.0. Should we all use ReentrantLock and discard synchronized?

Built-in locks still have significant advantages over display locks. The built-in lock is familiar to many developers and is compact and compact. ReentrantLock is much more dangerous than synchronization, and if you forget to call UNLOCK ina finally block, your code may appear to be working, but it’s actually a time bomb that can hurt other code. ReentrantLock should only be considered if the built-in lock does not meet your requirements.

Principle of use: ReentrantLock can be used as an advanced tool when advanced features such as timed, trainable, and interruptible lock acquisition operations, fair queues, and non-block locking are required. Otherwise, use synchronized in preference.

It is important to emphasize that synchronized and ReentrantLock are both reentrant locks. See article “Java Concurrent Programming Series 3” synchronized.

Read-write lock

Read/write locks are used in the same way as read/write locks in Go.

public interface ReadWriteLock {
    /** * return read lock */
    Lock readLock(a);
    /** * returns write lock */
    Lock writeLock(a);
}
Copy the code

ReadWriteLock manages a set of locks, a read-only lock and a write lock. ReetrantReadWriteLock implements the ReadWriteLock interface and adds reentrant features in the Java concurrency library.

Here’s a look at the posture:

public class ReadWriteMap<K.V> {
    private final Map<K,V> map;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock r = lock.readLock();
    private final Lock w = lock.writeLock();
    public ReadWriteMap(Map<K,V> map) {
        this.map = map;
    }
    public V put(K key, V value) {
        w.lock();
        try {
            return map.put(key,value);
        } finally{ w.unlock(); }}public V get(Object key) {
        r.lock();
        try {
            return map.get(key);
        } finally{ r.unlock(); }}}Copy the code

This allows multiple threads to read the data, but only one thread can write the data, and the reading and writing cannot take place simultaneously.

other

spinlocks

And just as an extension, I thought it was interesting, but what is a spin lock?

Spin lock definition: when a thread attempts to acquire a lock, if the lock has already been acquired by someone else, the thread cannot acquire the lock, and will wait for some time before attempting to acquire the lock again. This mechanism of cyclic locking -> waiting is called spinlock.

The principle of spin locking

Spinlocks principle is simple, if the thread holding the lock can lock is released in a short time resources, and the thread lock wait for competition there is no need to do between kernel mode and user mode switch into the blocking state, they just need to wait for a while (spin), until the thread holding the lock lock is released after available, thus avoiding the user process and the consumption of the kernel switch.

Because they avoid operating system process scheduling and thread switching, spin locks are usually appropriate for short periods of time. For this reason, operating system kernels often use spin locks. However, if locked for long periods of time, spin locks can be very costly in performance, preventing other threads from running and scheduling. The longer a thread holds the lock, the greater the risk that the thread holding the lock will be interrupted by the Operating System (OS) scheduler. If an interrupt occurs, the other threads will remain spinning (repeatedly trying to acquire the lock), and the thread holding the lock does not intend to release it, resulting in an indefinite delay until the thread holding the lock can complete and release it.

A good way to solve this problem is to set a spin time for the spin lock and release the spin lock immediately.

Advantages and disadvantages of spin locks

Spinlocks reduce thread blocking as much as possible, which is a big performance improvement for blocks that are less competitive for locks and have a very short lock time, because the spin cost is less than the cost of blocking, suspending and waking up operations that cause the thread to do two context switches!

But if the lock of the competition is intense, or thread holding the lock need long time to occupy the lock synchronization block, this time is not suitable for using a spin lock, because of the spin lock before acquiring a lock is CPU doing this all the time, for the XX XX, competition at the same time a large number of threads in a lock, and can lead to acquiring a lock for a long time, The cost of thread spin is greater than the cost of thread blocking and suspending operations, and other threads that need CPU cannot obtain CPU, resulting in CPU waste. So in this case we want to turn off the spin lock.

Implementation of spin lock

public class SpinLockTest {
    private AtomicBoolean available = new AtomicBoolean(false);
    public void lock(a){
        // Cyclic detection attempts to acquire the lock
        while(! tryLock()){// doSomething...}}public boolean tryLock(a){
        // Try to acquire the lock, return true on success, false on failure
        return available.compareAndSet(false.true);
    }
    public void unLock(a){
        if(! available.compareAndSet(true.false)) {throw new RuntimeException("Lock release failed"); }}}Copy the code

One problem with this simple spin lock is that it does not guarantee fairness in multithreaded competition. For the SpinlockTest above, when multiple threads want to acquire the lock, whoever sets available to false first will acquire the lock first. This may cause some threads to never acquire the lock, causing thread hunger. Just as we rush to the cafeteria after class and rush to the subway after work, we usually solve such problems by queuing. Similarly, we call this kind of lock a QueuedSpinlock. Computer scientists have used various methods to implement queued spinlocks, such as TicketLock, MCSLock, and CLHLock.

The characteristics of the lock

There are many locks in Java, and they can be classified according to different functions and types. Here is my classification of some common locks in Java, including some basic overview:

  • Whether threads need to lock resources can be divided into “pessimistic lock” and “optimistic lock”.
  • If the slave resource is locked, whether the thread is blocked can be classified as “spinlock”.
  • Concurrent access to resources from multiple threads, also known as Synchronized, can be classified as locking free, biased, lightweight, and heavyweight
  • The fairness of locks can be divided into “fair locks” and “unfair locks”.
  • It can be divided into “reentrant lock” and “non-reentrant lock” according to whether the lock is acquired repeatedly.
  • Whether multiple threads can acquire the same lock can be divided into “shared locks” and “exclusive locks”

Specific can refer to the article “do not understand what is the lock? Look at this you will understand” : mp.weixin.qq.com/s?__biz=Mzk…

conclusion

This article mainly explains the deadlock, deadlock solution, ReentrantLock, ReentrantLock and built-in lock synchronized comparison, finally also explains the spin lock, the previous content is the core part, the spin lock only as an extension of knowledge.

The lock content is now summarized, so I will first learn this area in the Java concurrent programming series, and will continue to maintain this series if I learn other Java concurrent knowledge. I have set a Flag for myself before, and I need to learn all the basic knowledge of Java this year, so my next series will be Spring. I hope that Java experts like me can make progress together.

Welcome everyone to like a lot, more articles, please pay attention to the wechat public number “Lou Zai advanced road”, point attention, do not get lost ~~