What exactly is a reentry lock? Please, find out at once!

Hello, I'm Lao Tian. Let's talk about re entering the lock today.

Recommended articles: Learn more about ReentrantLock

In addition to using the keyword synchronized, the implementation of exclusive lock in JDK can also use ReentrantLock. Although there is no difference in performance between ReentrantLock and synchronized, compared with synchronized, ReentrantLock has richer functions, is more flexible to use, and is more suitable for complex concurrent scenarios.

Similarities between the two

ReentrantLock is exclusive and reentrant

example

public class ReentrantLockTest {

    public static void main(String[] args) throws InterruptedException {

        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

            } finally {
                lock.unlock();
            }
        }
    }
}

The above code obtains the lock three times through the lock() method, and then releases the lock three times through the unlock() method. The program can exit normally. As can be seen from the above example, ReentrantLock is a lock that can be re entered. When a thread acquires a lock, it can acquire it repeatedly. In addition to the exclusivity of ReentrantLock, we can get the following similarities between ReentrantLock and synchronized.

  • ReentrantLock and synchronized are exclusive locks that only allow threads to access critical areas that are mutually exclusive. However, the implementation of the two is different: the synchronized locking and unlocking process is implicit, and the user does not need to operate manually. The advantage is that the operation is simple, but it is not flexible enough. In general, synchronized is enough for concurrent scenarios; ReentrantLock needs to be locked and unlocked manually, and the unlocking operation should be placed in the finally code block as far as possible to ensure that the thread releases the lock correctly. ReentrantLock operation is complex, but it can be used in complex concurrent scenarios because the locking and unlocking processes can be controlled manually.
  • ReentrantLock and synchronized are both reentrant. Because synchronized is reentrant, it can be placed on the method executed recursively, and there is no need to worry about whether the thread can release the lock correctly in the end; When ReentrantLock re enters, it must ensure that the number of times it repeatedly obtains the lock must be the same as the number of times it repeatedly releases the lock, otherwise it may cause other threads to fail to obtain the lock.

Additional features of both

ReentrantLock can realize fair lock

Fair lock means that when the lock is available, the thread waiting on the lock for the longest time will obtain the right to use the lock. Non fair locks are randomly assigned this right of use. Like synchronized, the default ReentrantLock implementation is a non fair lock, because non fair locks perform better than fair locks. Of course, fair locks can prevent hunger and are useful in some cases. When creating ReentrantLock, a fair lock is created by passing in the parameter true. If false is passed in or no parameter is passed in, a non fair lock is created

ReentrantLock lock = new ReentrantLock(true);

Continue to follow up and look at the source code

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

It can be seen that the key to the implementation of fair lock and non fair lock lies in the different implementation of member variable sync, which is the core of mutually exclusive synchronization. We'll talk more about it when we have a chance.

An example of a fair lock

public class ReentrantLockTest {

    static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override

      public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                lock.lock();
                System.out.println("Thread obtaining lock:"+id);
                lock.unlock();
            }
        }
    }
}

Fair lock result

We open five threads and let each thread acquire and release the lock twice. To better observe the results, let the thread sleep for 10 milliseconds before each lock acquisition. You can see that the threads almost take turns to get the lock. If we change to unfair lock, let's see the result

Unfair lock result

The thread repeatedly acquires the lock. If enough threads apply to acquire locks, some threads may not get locks for a long time. This is the "hunger" problem of non-public locks.

How to choose fair lock and unfair lock?

In most cases, we use unfair locks because their performance is much better than fair locks. However, fair locks can avoid thread starvation and are useful in some cases.

ReentrantLock responds to an interrupt

When using synchronized to implement a lock, the thread blocking on the lock will wait until it obtains the lock, that is, the behavior of waiting indefinitely to obtain the lock cannot be interrupted. ReentrantLock provides us with a lock acquisition method lockinterruptible () that can respond to interrupts. This method can be used to solve the deadlock problem.

Examples of responding to interruptions:

public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//The thread acquires lock 1 first and then lock 2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//The thread acquires lock 2 first and then lock 1
        thread.start();
        thread1.start();
        thread.interrupt();//Is the first thread interrupt
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                firstLock.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(10);//Better trigger deadlock
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"Normal end!");
            }
        }
    }
}

result:

Construct deadlock scenario:

Create two sub threads, which will attempt to obtain two locks respectively at run time. One thread first acquires lock 1 and then acquires lock 2. The other thread is just the opposite. If there is no external interrupt, the program will be in a deadlock state and can never stop. We end the meaningless wait between threads by interrupting one of the threads. The interrupted thread will throw an exception, and another thread will be able to get the lock and end normally.

3.3 time limit waiting for lock acquisition

ReentrantLock also provides us with a method tryLock() to wait when obtaining the lock limit. You can select the incoming time parameter to indicate the specified time to wait. If there is no parameter, it means that the result of the lock application is returned immediately: true means that the lock is obtained successfully and false means that the lock is obtained failed. We can use this method together with the failure retry mechanism to better solve the deadlock problem.

Examples of better deadlock resolution:

public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//The thread acquires lock 1 first and then lock 2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//The thread acquires lock 2 first and then lock 1
        thread.start();
        thread1.start();
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                while(!lock1.tryLock()){
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                while(!lock2.tryLock()){
                    lock1.unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"Normal end!");
            }
        }
    }
}

result:

The thread obtains the lock by calling the tryLock() method. When it fails to obtain the lock for the first time, it will sleep for 10 milliseconds, and then obtain it again until it succeeds. When the second acquisition fails, first release the first lock, then sleep for 10 milliseconds, and then try again until it succeeds. When a thread fails to obtain the second lock, it will release the first lock, which is the key to solving the deadlock problem. It avoids two threads holding one lock and then requesting another lock from each other.

Implementation of waiting notification mechanism combined with Condition

Using synchronized in combination with the wait and notify methods on the Object can implement the wait notification mechanism between threads. ReentrantLock and Condition interface can also realize this function. And compared with the former, it is clearer and simpler to use.

Introduction to Condition

Condition is created by ReentrantLock object, and multiple conditions can be created at the same time

static Condition notEmpty = lock.newCondition();

static Condition notFull = lock.newCondition();

Before using the condition interface, you must call the lock() method of ReentrantLock to obtain the lock. The await() that calls the Condition interface will release the lock and wait on the Condition until the other thread invokes the signal() method of Condition to wake the thread. The usage is similar to wait and notify.

A simple example of using condition:

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("Main thread waiting for notification");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("The main thread resumes running");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("Child thread notification");
            } finally {
                lock.unlock();
            }
        }
    }
}

Operation results:

Using Condition to implement a simple blocking queue

Blocking queue is a special first in first out queue. It has the following characteristics: 1. Incoming and outgoing threads are safe. 2. When the queue is full, incoming threads will be blocked; When the queue is empty, the outgoing thread is blocked.

Simple implementation of blocking queue:

public class MyBlockingQueue<E> {

    int size;//Maximum capacity of blocking queue

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//Queue underlying implementation

    Condition notFull = lock.newCondition();//Wait condition when queue is full
    Condition notEmpty = lock.newCondition();//Wait condition when queue is empty

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() ==size)//Queue full, waiting on notFull condition
                notFull.await();
            list.add(e);//Join the team: join the end of the linked list
            System.out.println("Join the team:" +e);
            notEmpty.signal(); //Notifies the thread waiting on the notEmpty condition
        } finally {
            lock.unlock();
        }
    }

    public E dequeue() throws InterruptedException {
        E e;
        lock.lock();
        try {
            while (list.size() == 0)//Queue is empty, waiting on notEmpty condition
                notEmpty.await();
            e = list.removeFirst();//Out of line: remove the first element of the linked list
            System.out.println("Out of the team:"+e);
            notFull.signal();//Notifies the thread waiting on the notFull condition
            return e;
        } finally {
            lock.unlock();
        }
    }
}

Test code

public static void main(String[] args) throws InterruptedException {

    MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
    for (int i = 0; i < 10; i++) {
        int data = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.enqueue(data);
                } catch (InterruptedException e) {

                }
            }
        }).start();

    }
    for(int i=0;i<10;i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer data = queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

Operation results:

summary

ReentrantLock is a reentrant exclusive lock. Compared with synchronized, it has richer functions, supports fair lock implementation, interrupt response and time limited waiting, etc. You can easily implement the waiting notification mechanism in conjunction with one or more Condition conditions.

Well, that's all for today.

Reference: www.cnblogs.com/takumicx

Posted on Fri, 26 Nov 2021 11:49:17 -0500 by lordzardeck