[Java Concurrent Programming Series 9] lock

Posted by tommy1987 on Wed, 29 Dec 2021 21:41:51 +0100

It mainly explains the common locks in Java.

preface

The series of concurrent programming should be coming to an end. Locks may be the last article in this series, and important basic knowledge should be covered. Then, for the last few chapters of the book "practical combat of Java Concurrent Programming", I only read the lock part. This article mainly summarizes the contents of the lock in the book.

deadlock

Deadlock refers to the phenomenon that a group of threads competing for resources wait for each other, resulting in "permanent" blocking.

Lock-Ordering Deadlock

Let's take a look at an example of deadlock. First, we define a BankAccount object to store basic information. The code is as follows:

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

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);
                }
            }
        }
    }
}

A deadlock occurs if the following calls are made:

transfer(myAccount, yourAccount, 10);
transfer(yourAccount, myAccount, 10);

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

Deadlock avoidance through sequence

Since we cannot control the order of parameters, if we want to solve this problem, we must define the order of locks and obtain locks in this order throughout the application. We can use object The value returned by hashcode to define the lock order:

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);
                        }
                    }
                }
            }
        }
    }
}

No matter how your input parameter changes, we always lock the data with small hash value first and then the data with large hash value through the size of hash value, so as to ensure the order of locks.

However, in rare cases, the Hash values of two objects are the same. If the order is wrong, it may still lead to deadlock. Therefore, before obtaining two locks, Using the tie breaking lock ensures that only one thread obtains the lock in an unknown order at a time. However, if the program often has Hash conflicts, it will become a bottleneck of concurrency, because the final variable is visible in memory, which will block all threads to the lock, but the probability is very low.

Deadlock between collaboration objects

I'll just briefly explain here that there are two objects a and B, A.action_A1() will call the method action in B_ B1 (), and B.action_B2() will call the method action in a_ A2 (), because these four methods are action_A1(),action_A2(),action_B1(),action_B2() locks through synchronized. We know that object locks are added to methods through synchronized. Therefore, when a calls B's method, B is also calling a's method, resulting in deadlock waiting for each other.

For specific examples, you can refer to page 174 of the Book Java Concurrent Programming practice.

ReentrantLock

usage method

The only mechanisms that can be used to coordinate the access of objects are synchronized and volatile. Java 5.0 adds a new mechanism: ReentrantLock. ReentrantLock is not a method to replace the built-in lock, but an optional advanced function when the built-in lock mechanism is not applicable.

Here is a simple example:

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

In addition to the above, the reason why synchronized cannot be replaced is that you need to manually pass lock Unlock () releases the lock. If you forget to release it, it will be a very serious problem.

Avoiding sequence deadlock through tryLock

Still follow the deadlock example above, let's make a simple transformation through 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
    }
}

We first try to obtain the lock of sourceAccount. If the acquisition is successful, we then try to obtain the lock of targetAccount. If the acquisition fails, we release the lock of sourceAccount to avoid deadlock caused by long-term occupation of sourceAccount lock.

Locking with time limit

We can also specify the timeout time for tryLock(). If the waiting time expires, we will not wait all the time, and directly execute the following logic:

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

synchronized vs ReentrantLock

ReentrantLock provides the same semantics on locking and memory as built-in locks. In addition, it also provides some other functions, including timed lock waiting, interruptible lock waiting, fairness, and locking in non block structure. The performance of ReentrantLock seems to be better than the built-in lock, which is slightly better in Java 6.0 and far better in Java 5.0. Do we all use ReentrantLock and directly discard synchronized?

Compared with the display lock, the built-in lock still has great advantages. Built in locks are familiar to many developers and are simple and compact. ReentrantLock is more dangerous than synchronization. If you forget to call unlock in the finally block, though the code looks normal on the surface, it has actually planted a time bomb and is likely to hurt other code. Reentrantlock can be used only when the built-in lock cannot meet the requirements.

Usage principle: ReentrantLock can be used as an advanced tool when some advanced functions are required, such as timed, rotatable and interruptible lock acquisition operations, fair queues, and non block locks. Otherwise, synchronized is preferred.

Then there is a point to emphasize. Synchronized and ReentrantLock are reentrant locks. Please refer to the article [Java Concurrent Programming Series 3] synchronized.

Read write lock

The use of read-write lock is consistent with that in Go. First, see the definition of read-write lock interface:

public interface ReadWriteLock {
    /**
     * Return read lock
     */
    Lock readLock();
    /**
     * Return write lock
     */
    Lock writeLock();
}

ReadWriteLock manages a set of locks, one is a read-only lock and the other is a write lock. The ReadWriteLock interface is implemented in the Java concurrency library, and the reentrant feature is added.

Let's take 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();
        }
    }
}

In this way, multiple threads can read data, but only one thread can write data, and then reading and writing cannot be carried out at the same time.

other

Spin lock

This is just an extension of knowledge. If you think it has some meaning, write it in. So what is spin lock?

Definition of spin lock: when a thread attempts to acquire a lock, if the lock has been acquired (occupied) by others, the thread cannot acquire the lock. The thread will wait and try to acquire it again after an interval of time. This mechanism of cyclic locking - > waiting is called spin lock.

Principle of spin lock

The principle of spin lock is relatively simple. If the thread holding the lock can release the lock resources in a short time, the threads waiting for the competing lock do not need to switch between the kernel state and the user state to enter the blocking state. They only need to wait (spin) until the thread holding the lock releases the lock, so as to avoid the consumption of user process and kernel switching.

Because spin lock avoids operating system process scheduling and thread switching, spin lock is usually applicable in the case of short time. For this reason, the operating system kernel often uses spin locks. However, if locked for a long time, spin lock will be very performance-consuming, which prevents other threads from running and scheduling. The longer a thread holds a lock, the greater the risk that the thread holding the lock will be interrupted by the OS(Operating System) scheduler. If an interrupt occurs, other threads will remain rotated (repeatedly trying to acquire the lock), and the thread holding the lock does not intend to release the lock. As a result, the result is an indefinite delay until the thread holding the lock can complete and release it.

A good way to solve the above situation is to set a spin time for the spin lock and release the spin lock as soon as the time comes.

Advantages and disadvantages of spin lock

Spin lock reduces thread blocking as much as possible, which can greatly improve the performance of code blocks that do not compete fiercely and occupy a very short lock time, because the consumption of spin will be less than that of thread blocking, hanging and waking up operations, which will lead to two context switches in the line!

However, if the lock competition is fierce, or the thread holding the lock needs to occupy the lock for a long time to execute the synchronization block, it is not suitable to use the spin lock, because the spin lock always occupies the cpu for useless work before obtaining the lock, accounting for XX or XX. At the same time, a large number of threads compete for a lock, which will lead to a long time to obtain the lock, The consumption of thread spin is greater than that of thread blocking and pending operation, and other threads that need cpu can not obtain cpu, resulting in a waste of cpu. So in this case, we have to turn off the spin lock.

Implementation of spin lock

public class SpinLockTest {
    private AtomicBoolean available = new AtomicBoolean(false);
    public void lock(){
        //Loop detection attempts to acquire locks
        while (!tryLock()){
            // doSomething...
        }
    }
    public boolean tryLock(){
        //When trying to obtain a lock, it returns true if successful, and false if failed
        return available.compareAndSet(false,true);
    }
    public void unLock(){
        if(!available.compareAndSet(true,false)){
            throw new RuntimeException("Failed to release lock");
        }
    }
}

This simple spin lock has a problem: it can not guarantee the fairness of multi-threaded competition. For the SpinlockTest above, when multiple threads want to obtain a lock, the one who first sets available to false can obtain the lock first, which may cause some threads to never obtain the lock and cause thread hunger. Just like we rush to the canteen after class and rush to the subway after work, we usually solve this problem by queuing. Similarly, we call this kind of lock queued spinlock. Computer scientists have used various ways to implement queued spin locks, such as TicketLock, MCSLock and CLHLock.

Lock characteristics

There are many locks in Java, which can be classified according to different functions and types. The following is my classification of some common locks in Java, including some basic overviews:

  • Whether a thread needs to lock resources can be divided into "pessimistic lock" and "optimistic lock"

  • Since the resource has been locked, whether the thread is blocked can be divided into "spin lock"

  • Concurrent access to resources from multiple threads, that is, Synchronized, can be divided into lockless, biased, lightweight and heavyweight locks

  • From the perspective of fairness, locks can be divided into "fair locks" and "unfair locks"

  • According to whether the lock is acquired repeatedly, it can be divided into "reentrant lock" and "non reentrant lock"

  • Whether the same lock can be obtained from multiple threads is divided into "shared lock" and "exclusive lock"

For details, please refer to the article "don't know what a lock is? Just look at this article and you will understand it": https://mp.weixin.qq.com/s?__biz=MzkwMDE1MzkwNQ==&mid=2247496038&idx=1&sn=10b96d79a1ff5a24c49523cdd2be43a4&chksm=c04ae638f73d6f2e1ead614f2452ebaeab26cf77b095d6f634654699a1084365e7f5cf6ca4f9&token=1816689916&lang=zh_CN#rd

summary

This article mainly explains deadlocks, solutions to deadlocks, the comparison between ReentrantLock, ReentrantLock and synchronized built-in locks, and finally explains spin locks. The previous content is the core part, and spin locks are only used as extended knowledge.

The contents of the lock have been summarized, so I will learn this part of the Java Concurrent Programming Series first. If I learn other Java Concurrent knowledge later, I will continue to maintain this series. I set myself a Flag before. I need to learn all the basic knowledge of Java this year, so my next series will be Spring. I hope Java Xiaobai like me can make progress together.

Welcome to the many more articles, please pay attention to the official account of WeChat public, "the road of Lou Tsai", pay attention to it and not lose your way.

Topics: Java Concurrent Programming