Reentrantlock details

Keywords: Programming Java JDK jvm

Reprint link: https://www.cnblogs.com/takumicx/p/9338983.html

About ReentrantLock

The implementation of exclusive lock in jdk can use ReentrantLock in addition to the keyword synchronized. Although there is no difference between ReentrantLock and synchronized in performance, ReentrantLock has more functions, is more flexible in use, and is more suitable for complex concurrent scenarios than synchronized.

Similarities between ReentrantLock and synchronized

public class ReentrantLockTest {

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();

        for (int i = 0; i < 3; i++) {
            lock.lock();
            System.out.println("Get lock:" + i);
        }

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

            } finally {
                lock.unlock();
                System.out.println("Release lock:" + i);
            }
        }
        System.out.println("main Continue");
    }
}

The above code first 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. It shows that ReentrantLock is a lock that can be re entered. When a thread acquires a lock, it can then acquire it repeatedly. In addition to the exclusivity of ReentrantLock, we can get the following similarities between ReentrantLock and synchronized.

  • 1.ReentrantLock and synchronized are exclusive locks, which only allow threads to access critical areas mutually exclusive. But they are different in implementation: synchronized is implemented at the JVM level, and lock will be released automatically in case of code execution exception. ReentrantLock is a class implemented by Java code. To release lock, unlock method must be executed in finally {}.
  • 2.ReentrantLock and synchronized are reentrant. Synchronized because it can be reentrant, it can be placed on the method that is executed recursively, and it is not necessary to worry about whether the thread can release the lock correctly in the end; while ReentrantLock needs to ensure that the number of times to repeatedly acquire the lock is the same as the number of times to repeatedly release the lock when it is reentrant, otherwise other threads may not be able to acquire the lock.

Additional features of ReentrantLock compared to synchronized

ReentrantLock can realize fair lock

Fair lock means that when the lock is available, the thread with the longest waiting time on the lock will get the right to use the lock. Non fair locks are randomly assigned this right of use. As with synchronized, the default ReentrantLock implementation is an unfair lock because it performs better than fair locks. Of course, fair locks can prevent hunger, and in some cases can be useful. When creating ReentrantLock, a fair lock is created by passing in the parameter true. If the parameter passed in is false or not, an unfair lock is created.

An example of a fair lock:

public class ReentrantLockTest {

    private static Lock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 2; j++) {
                    lock.lock();
                    System.out.println("Thread getting lock:" + Thread.currentThread().getName());
                    lock.unlock();
                }
            }).start();
        }
    }
}

Output results:

Thread obtaining lock: Thread-1
 Thread obtaining lock: Thread-2
 Thread obtaining lock: Thread-0
 Thread obtaining lock: Thread-3
 Thread obtaining lock: Thread-4
 Thread obtaining lock: Thread-1
 Thread obtaining lock: Thread-2
 Thread obtaining lock: Thread-0
 Thread obtaining lock: Thread-3
 Thread obtaining lock: Thread-4

We open 5 threads and let each thread acquire the release lock twice. In order to better observe the results, let the thread sleep for 10 milliseconds before each lock acquisition. It can be seen that the threads acquire the lock almost in turn (the order of acquiring the lock for the second time is the same as that of acquiring the lock for the first time).

If we change to unfair lock, let's look at the following results:

Thread obtaining lock: Thread-0
 Thread obtaining lock: Thread-0
 Thread obtaining lock: Thread-2
 Thread obtaining lock: Thread-2
 Thread obtaining lock: Thread-4
 Thread obtaining lock: Thread-4
 Thread obtaining lock: Thread-1
 Thread obtaining lock: Thread-1
 Thread obtaining lock: Thread-3
 Thread obtaining lock: Thread-3

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

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

ReentrantLock responds to interrupts

When using synchronized to implement the lock, the thread blocking on the lock will wait until it obtains the lock, that is to say, the infinite wait for obtaining the lock cannot be interrupted. ReentrantLock gives us a way to acquire locks in response to interrupts. This method can be used to solve the deadlock problem.

public class ReentrantLockTest {

    private static Lock lockFirst = new ReentrantLock();
    private static Lock lockSecond = new ReentrantLock();

    public static void main(String[] args) {
        //This thread (Thread-0) obtains lock 1, then lock 2
        Thread threadFirst = new Thread(new ThreadTest(lockFirst, lockSecond));
        //The Thread-1 obtains lock 2 first, then lock 1
        Thread threadSecond = new Thread(new ThreadTest(lockSecond, lockFirst));
        threadFirst.start();
        threadSecond.start();
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //Thread-0 interrupt, release lock
        threadFirst.interrupt();
    }

    private static class ThreadTest implements Runnable {

        private Lock lockFirst;
        private Lock lockSecond;

        public ThreadTest(Lock lockFirst, Lock lockSecond) {
            this.lockFirst = lockFirst;
            this.lockSecond = lockSecond;
        }

        @Override
        public void run() {
            try {
                lockFirst.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "Get the first lock!");
                TimeUnit.MILLISECONDS.sleep(10);
                lockSecond.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + "Get the second lock!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lockFirst.unlock();
                lockSecond.unlock();
                System.out.println(Thread.currentThread().getName() + "Lock released normally!");
            }
            System.out.println(Thread.currentThread().getName() + "Normal end!");
        }
    }
}

Construct deadlock scenario: create two sub threads, which will try to acquire two locks respectively at runtime. One thread first acquires lock 1 and then acquires lock 2. The other thread is the opposite. If there is no external interruption, the program will be in a deadlock state and can never be stopped. We end the meaningless wait between threads by interrupting one of them. The interrupted thread will throw an exception, while another thread will be able to get the lock and end normally.

Operation result:

Thread-0 Get the first lock!
Thread-1 Get the first lock!
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
Thread-1 Get the second lock!
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
Thread-1 Lock released normally!
	at java.lang.Thread.run(Thread.java:748)
Thread-1 Normal end!

Time limit waiting for lock acquisition

ReentrantLock also provides us with tryLock(), a method for obtaining lock time limit waiting. You can choose to pass in a time parameter to indicate waiting for the specified time. If there is no parameter, the result of lock application will be returned immediately: true indicates the success of lock acquisition, false indicates the failure of lock acquisition. We can use this method with the failure retry mechanism to better solve the deadlock problem.

public class ReentrantLockTest {

    private static Lock lockFirst = new ReentrantLock();
    private static Lock lockSecond = new ReentrantLock();

    public static void main(String[] args) {
        //This thread (Thread-0) obtains lock 1, then lock 2
        Thread threadFirst = new Thread(new ThreadTest(lockFirst, lockSecond));
        //The Thread-1 obtains lock 2 first, then lock 1
        Thread threadSecond = new Thread(new ThreadTest(lockSecond, lockFirst));
        threadFirst.start();
        threadSecond.start();
    }

    private static class ThreadTest implements Runnable {

        private Lock lockFirst;
        private Lock lockSecond;

        public ThreadTest(Lock lockFirst, Lock lockSecond) {
            this.lockFirst = lockFirst;
            this.lockSecond = lockSecond;
        }

        @Override
        public void run() {
            try {
                while (!lockFirst.tryLock()) {
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                System.out.println(Thread.currentThread().getName() + "Get the first lock!");
                while (!lockSecond.tryLock()) {
                    lockFirst.unlock();
                    System.out.println(Thread.currentThread().getName() + "Failed to acquire the second lock, release the first lock!");
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                System.out.println(Thread.currentThread().getName() + "Get the second lock!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                try {
                    lockFirst.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                try {
                    lockSecond.unlock();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "Lock released normally!");
            }
            System.out.println(Thread.currentThread().getName() + "Normal end!");
        }
    }
}

The thread obtains the lock by calling the tryLock() method. If it fails to acquire the first lock, it will sleep for 10 milliseconds, and then acquire it again until it succeeds. When obtaining the second failure, the first lock will be released first, then sleep for 10 milliseconds, and then try again until success. When the thread fails to acquire the second lock, it will release the first lock, which is the key to solve the deadlock problem, avoiding two threads holding one lock respectively and then requesting another lock from each other.

Operation result:

Thread-0 Get the first lock!
Thread-1 Get the first lock!
Thread-0 Failed to acquire the second lock, release the first lock!
Thread-1 Get the second lock!
Thread-1 Lock released normally!
Thread-1 Normal end!
Thread-0 Get the second lock!
java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at java.lang.Thread.run(Thread.java:748)
Thread-0 Lock released normally!
Thread-0 Normal end!

Implementation of waiting notification mechanism with Condition

Using synchronized combined with wait and notify methods on Object can realize waiting notification mechanism between threads. ReentrantLock in combination with the Condition interface can also achieve this function. Compared with the former, it is clearer and easier to use.

Introduction to Condition

Conditions are created by ReentrantLock objects, and multiple can be created at the same time. The condition interface must call the lock() method of ReentrantLock to obtain the lock before using it. 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. It is similar to wait and notify.

public class ConditionTest {

    private static Lock lock = new ReentrantLock();

    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {

        lock.lock();
        System.out.println(Thread.currentThread().getName() + "Get lock");
        new Thread(() -> {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "Get lock");
            try {
                TimeUnit.SECONDS.sleep(3);
                condition.signal();
                System.out.println(Thread.currentThread().getName() + "notice");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "Release lock");
            }
        }).start();

        try {
            System.out.println(Thread.currentThread().getName() + "Waiting notification");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "Resume operation");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + "Release lock");
        }

    }
}

Operation result:

main get lock
 main waiting for notification
 Thread-0 acquire lock
 Thread-0 notification
 Thread-0 release lock
 main resume operation
 main release lock

Using Condition to realize simple blocking queue

Blocking queue is a special first in, first out queue, which has the following characteristics:

  • Entry and exit thread safety
  • When the queue is full, the incoming thread will be blocked; when the queue is empty, the outgoing thread will be blocked.

Simple implementation of blocking queue:

public class MyBlockingQueue<E> {

    //Maximum capacity of blocking queue
    private int size;

    private Lock lock = new ReentrantLock();

    //The underlying implementation of queue
    LinkedList<E> list = new LinkedList<>();

    //Waiting conditions when the queue is full
    Condition notFull = lock.newCondition();
    //Waiting conditions when the queue is empty
    Condition notEmpty = lock.newCondition();

    //Waiting conditions when the queue is empty
    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) {
        lock.lock();
        try {
            //The queue is full, wait on the notFull condition, wait for the moment when the queue is not satisfied
            while (list.size() == size) {
                notFull.await();
            }
            //Join the team and join the end of the list
            list.add(e);
            System.out.println("Team entry:" + e);
            //Notifies threads waiting on a notEmpty condition
            notEmpty.signal();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public E dequeue() {
        lock.lock();
        E e;
        try {
            //The queue is empty, wait on the notEmpty condition, wait for the moment when it is not empty
            while (list.size() == 0) {
                notEmpty.await();
            }
            //Out of line, remove the first element of the list
            e = list.removeFirst();
            System.out.println("Team:" + e);
            //Notify threads waiting on notFull condition
            notFull.signal();
            return e;
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            lock.unlock();
            return null;
        }
    }
}

Test:

public class BlockingQueueTest {

    public static void main(String[] args) {

        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);

        //10 thread production
        for (int i = 0; i < 10; i++) {
            int data = i;
            new Thread(() -> {
                queue.enqueue(data);
                System.out.println(Thread.currentThread().getName() + "Production:" + data);
            }).start();
        }

        //10 thread consumption
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Integer data = queue.dequeue();
                System.out.println(Thread.currentThread().getName() + "Consumption:" + data);
            }).start();
        }
    }
}

Operation result:

Team entry: 0
 Team entry: 9
 Thread-9 production: 9
 Thread-0 production: 0
 Team: 0
 Thread-10 consumption: 0
 Team entry: 3
 Thread-3 production: 3
 Team: 9
 Team: 3
 Thread-11 consumption: 9
 Team entry: 4
 Thread-13 consumption: 3
 Team entry: 1
 Thread-4 production: 4
 Team: 4
 Thread-1 production: 1
 Team: 1
 Thread-16 consumption: 4
 Thread-17 consumption: 1
 Team entry: 2
 Thread-2 production: 2
 Team: 2
 Thread-18 consumption: 2
 Team entry: 6
 Thread-6 production: 6
 Team: 6
 Thread-14 consumption: 6
 Team entry: 5
 Thread-5 production: 5
 Team: 5
 Thread-12 consumption: 5
 Team entry: 7
 Thread-7 production: 7
 Team: 7
 Thread-15 consumption: 7
 Team entry: 8
 Thread-8 production: 8
 Team: 8
 Thread-19 consumption: 8

summary

ReentrantLock is a reentrant exclusive lock. Compared with synchronized, it has richer functions, supports fair lock implementation, interrupt response, time limited waiting, etc. The waiting notification mechanism can be easily implemented with one or more Condition conditions.

Posted by haku87 on Tue, 19 Nov 2019 08:36:01 -0800