JUC learning - ReentrantLock

1, ReentrantLock in JUC

1. Limitations of synchronized

Synchronized is a built-in keyword in java, which provides an exclusive locking method. The synchronized acquisition and release locks are implemented by the jvm. Users do not need to display the released locks, which is very convenient. However, synchronized also has some limitations, such as:

  1. When a thread attempts to acquire a lock, if it cannot acquire the lock, it will be blocked all the time. This blocking process is beyond the control of the user
  2. If the thread that acquires the lock enters sleep or blocks, other threads must wait until they attempt to acquire the lock unless the current thread is abnormal

JDK1.5 was released after that, and the java.util.concurrent package implemented by Doug Lea was added. The lock class is provided in the package to provide more extended locking functions. Lock makes up for the limitation of synchronized and provides more fine-grained locking function.

2. ReentrantLock's understanding

ReentrantLock is the default implementation of Lock. Before talking about ReentrantLock, we need to clarify some concepts:

  1. Reentrant lock: a reentrant lock means that the same thread can obtain the same lock multiple times; ReentrantLock and the keyword Synchronized are both reentrant locks
  2. Interruptible lock: an interruptible lock refers to whether a child thread can interrupt the operation of the corresponding thread during the process of obtaining the lock. synchronized is non interruptible and ReentrantLock is interruptible
  3. Fair lock and unfair lock: when multiple threads attempt to obtain the same lock, the order of obtaining the lock is obtained according to the order in which the threads arrive, rather than randomly jumping in the queue. synchronized is a non fair lock, and ReentrantLock can be implemented in both, but the default is a non fair lock

3. ReentrantLock basic use

We use three threads to operate on a shared variable + +, first implemented with synchronized, and then implemented with ReentrantLock.

  • synchronized mode:
public class SynchronizedDemo {
    
    private static int num = 0;
    private static synchronized void add() {
        num++;
    }
    
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                SynchronizedDemo.add();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(SynchronizedDemo.num);
    }
}
  • ReentrantLock mode:
public class ReentrantLockDemo {
    
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    
    private static void add() {
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
        }
    }
    
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                ReentrantLockDemo.add();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(ReentrantLockDemo.num);
    }
}

How to use ReentrantLock:

  1. Create lock: ReentrantLock lock = new ReentrantLock();
  2. Get lock: lock.lock();
  3. Release lock: lock.unlock();

Compared with the above code, the ReentrantLock lock has an obvious operation process compared with the keyword synchronized. Developers must manually specify when to add the lock and when to release the lock. It is precisely because of this manual control that ReentrantLock is much more flexible in logical control than the keyword synchronized. The above code should note that lock.unlock() must be placed in finally, otherwise, If an exception occurs in the program and the lock is not released, other threads will never have a chance to acquire the lock.

4. ReentrantLock is a reentrant lock

Let's verify that ReentrantLock is a reentrant lock. Example code:

public class ReentrantLockDemo {
    private static int num = 0;
    private static ReentrantLock lock = new ReentrantLock();
    
    private static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }
    
    public static class T extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                ReentrantLockDemo.add();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        T t2 = new T();
        T t3 = new T();
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println(ReentrantLockDemo.num);
    }
}

In the add() method in the above code, when a thread enters, it will execute the operation of obtaining the lock twice. The running program can end normally and output 30000 as expected.

If ReentrantLock is a non reentrant lock, when the same thread obtains the lock for the second time, the program cannot end normally because the previous lock has not been released.

ReentrantLock is also well named. Like its name, re reentrant lock can re-enter the lock.

There are several points to note in the code:

  1. The lock() method and the unlock() method need to appear in pairs. If they are locked several times, they should also be released several times. Otherwise, the subsequent threads cannot obtain the lock; You can delete an unlock in add and try. The operation of the above code will not end
  2. The unlock() method is executed in finally to ensure that the lock will be released no matter whether the program is abnormal or not

5. ReentrantLock implements fair lock

In most cases, the application for lock is unfair, that is, thread 1 first requests lock A, and then thread 2 also requests lock A.

So when lock A is available, can thread 1 get the lock or thread 2 get the lock? This is not necessarily true. The system only randomly selects one from the waiting queue of this lock, so its fairness cannot be guaranteed.

This is like buying a ticket without queuing. Everyone is gathered in front of the ticket window. The conductor is busy. He doesn't care who comes first and who comes later. He can find someone to issue a ticket. The final result is that some people may not be able to buy a ticket all the time.

This is not the case with fair locks, which obtain resources in the order of arrival.

A major feature of fair lock is that it will not produce hunger. As long as you queue up, you can finally wait for resources; The synchronized keyword is controlled by the jvm by default and is a non fair lock. ReentrantLock sets the fairness of the lock by the running developer.

Take a look at the source code of ReentrantLock in jdk. There are two construction methods:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

The default constructor creates a non fair lock.

The second construction method has a fair parameter. When fair is true, a fair lock is created. The fair lock looks very good. However, to implement a fair lock, an ordered queue must be maintained within the system. Therefore, the implementation cost of a fair lock is relatively high and the performance is relatively low compared with that of a non fair lock.

Therefore, by default, locks are unfair. If there are no special requirements, fair locks are not recommended.

Fair locks and non fair locks are very different in program scheduling. Take a fair lock example:

public class FairLockDemo {
    private static int num = 0;
    private static ReentrantLock fairLock = new ReentrantLock(true);
    
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                fairLock.lock();
                try {
                    System.out.println(this.getName() + "Acquire lock!");
                } finally {
                    fairLock.unlock();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }
}

Run the above code and output the result:

t1 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!
t1 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!
t1 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!
t1 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!
t1 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!

Look at the output. The locks are obtained in order.

Modify the above code to try non fair lock, as follows:

ReentrantLock fairLock = new ReentrantLock(false);

The operation results are as follows:

t1 Acquire lock!
t3 Acquire lock!
t3 Acquire lock!
t3 Acquire lock!
t3 Acquire lock!
t1 Acquire lock!
t1 Acquire lock!
t1 Acquire lock!
t1 Acquire lock!
t2 Acquire lock!
t2 Acquire lock!
t2 Acquire lock!
t2 Acquire lock!
t2 Acquire lock!
t3 Acquire lock!

It can be seen that t3 may obtain locks continuously, and the result is random and unfair.

6. ReentrantLock the process of obtaining a lock is interruptible

For the synchronized keyword, if a thread is waiting to acquire a lock, there are only two results:

  1. Either get the lock and continue with the following operation
  2. Or wait until another thread releases the lock

ReentrantLock provides another possibility, that is, it can be interrupted during the process of waiting to obtain the lock (from the time when the request to obtain the lock is initiated to the time when the lock has not been obtained), that is, during the process of waiting for the lock, the program can cancel the request to obtain the lock as needed. Some use this operation is very necessary.

For example, you have an appointment with a good friend to play ball together. If you wait for half an hour and your friend hasn't arrived, suddenly you receive a call. Your friend can't come because of an emergency, then you must go home.

Interrupt operation provides a similar mechanism. If a thread is waiting to obtain a lock, it can still receive a notification that it can stop working without waiting.

Example code:

public class LockInterruptedDemo {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    private static ReentrantLock lock2 = new ReentrantLock(false);
    
    public static class T extends Thread {
        int lock;
        public T(String name, int lock) {
            super(name);
            this.lock = lock;
        }
        
        @Override
        public void run() {
            try {
                if (this.lock == 1) {
                    lock1.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                System.out.println("Interrupt flag:" + this.isInterrupted());
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1", 1);
        T t2 = new T("t2", 2);
        t1.start();
        t2.start();
    }
}

First run the above code and find that the program cannot end. Use jstack to view the thread stack information and find that two threads are deadlocked.

Found one Java-level deadlock:
=============================
"t2":
  waiting for ownable synchronizer 0x00000000d607f6d0, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t1"
"t1":
  waiting for ownable synchronizer 0x00000000d607f700, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
  which is held by "t2"

Lock1 is occupied by thread t1 and lock2 is occupied by thread t2. Thread t1 is waiting to acquire lock2 and thread t2 is waiting to acquire lock1. Both of them are waiting to acquire locks held by each other, resulting in a deadlock. If a deadlock occurs in the case of synchronized keyword, the program cannot end.

Let's modify the above code. Thread t2 has been unable to obtain lock1. After waiting for 5 seconds, we interrupt the operation of obtaining the lock. Mainly modify the main method as follows:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

Two lines of code timeunit.seconds.sleep (5) are added; t2.interrupt();, The program can be finished, and the running results are as follows:

Interrupt flag:false
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.juc.example.LockInterruptedDemo$T.run(LockInterruptedDemo.java:33)

As can be seen from the above information, the exception is triggered in line 33 of the code, and the interrupt flag is output: false

t2 failed to obtain the lock of lock1 in line 33. After waiting for 5 seconds in the main thread, t2 thread called the interrupt() method and set the interrupt flag of the thread to true. At this time, line 33 will trigger the InterruptedException exception. Then thread t2 can continue to execute downward and release the lock of lock2. Then thread t1 can obtain the lock normally, and the program can continue. After the thread sends an interrupt signal to trigger the InterruptedException exception, the interrupt flag will be cleared.

The process of obtaining locks is interrupted. Please pay attention to the following points:

  1. When the instance method lockInterruptibly() must be used in ReentrankLock to obtain a lock, the InterruptedException exception will not be thrown until the thread calls the interrupt() method
  2. After the thread calls interrupt(), the interrupt flag of the thread will be set to true
  3. After the InterruptedException exception is triggered, the interrupt flag of the thread will be cleared, that is, set to false
  4. Therefore, when a thread calls interrupt() to cause an InterruptedException exception, the change of the interrupt flag is: false - > true - > false

7. ReentrantLock lock application waiting time limit

  • What does it mean to apply for lock waiting time?

In general, we don't know the time to obtain the lock. In the process of obtaining the lock with the synchronized keyword, we can only wait for other threads to release the lock before we have the opportunity to obtain the lock. Therefore, the time to obtain the lock varies from long to short. It would be great if the time to acquire the lock could set the timeout.

ReentrantLock just provides such a function. It provides us with the method tryLock() to wait when obtaining the lock limit. You can select the incoming time parameter to indicate the waiting time. If there is no parameter, it means that the result of lock application is returned immediately: true means that the lock is obtained successfully, and false means that the lock is obtained failed.

7.1 tryLock parameterless method

Take a look at the tryLock method in the source code:

public boolean tryLock()

Returns a value of boolean type. This method will return immediately. The result indicates whether the lock acquisition is successful. Example:

public class TryLockTest {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Start acquiring lock!");
           
                if (lock1.tryLock()) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Lock acquired!");
                    //After obtaining the lock, sleep for 5 seconds
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Failed to acquire lock!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

After successfully obtaining the lock in the code, sleep for 5 seconds, which will cause another thread to fail to obtain the lock. Run the code and output:

1637821272586:t1 Start acquiring lock!
1637821272586:t1 Lock acquired!
1637821272587:t2 Start acquiring lock!
1637821272587:t2 Failed to acquire lock!

You can see that t1 acquisition succeeds and t2 acquisition fails. tryLock() responds immediately without blocking.

7.2 tryLock parametric method

You can explicitly set the timeout for obtaining locks. The method is as follows:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
  • This method will return the result no matter whether the lock can be obtained within the specified time. Returning true means that the lock is obtained successfully, and returning false means that the lock is obtained failed.

  • This method has two parameters. The second parameter is a time type, which is an enumeration. It can represent the waiting time of hour, minute, second and millisecond. It is convenient to use. The first parameter represents the length of time in the time type.

  • During the execution of this method, if the interrupt() method of the thread is called, an InterruptedException exception will be triggered.

Example code:

public class TryLockTest {
    private static ReentrantLock lock1 = new ReentrantLock(false);
    
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Start acquiring lock!");
                // The lock acquisition timeout is set to 3 seconds. Whether or not the lock can be acquired within 3 seconds will be returned
                if (lock1.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Lock acquired!");
                    //After obtaining the lock, sleep for 5 seconds
                    TimeUnit.SECONDS.sleep(5);
                } else {
                    System.out.println(System.currentTimeMillis() + ":" + this.getName() + "Failed to acquire lock!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();
    }
}

The ReentrantLock instance method tryLock(3, TimeUnit.SECONDS) is invoked in the program, which means that the timeout time of the lock is 3 seconds. After 3 seconds, whether or not it can get the lock, the method will have a return value. After obtaining the lock, the internal sleep for 5 seconds will cause another thread to get the lock failure.

Run the above code and output the result:

1637821925409:t1 Start acquiring lock!
1637821925410:t2 Start acquiring lock!
1637821925412:t2 Lock acquired!
1637821928413:t1 Failed to acquire lock!

According to the analysis of the output results, t2 obtains the lock, and then sleeps for 5 seconds. t1 fails to obtain the lock, and t1 prints 2 messages, with a time difference of about 3 seconds.

The tryLock() method and trylock (long timeout, timeunit) method are described as follows:

  1. Will return a boolean value, and the result indicates whether the lock acquisition is successful;
  2. The tryLock() method will return immediately whether it is successful or not; The parameterized tryLock method will try to obtain the lock within the specified time, and blocking will occur in the middle. After the specified time, the result will be returned regardless of whether the lock can be obtained or not;
  3. The tryLock() method does not respond to the thread's interrupt method; The parameterized tryLock method will respond to the thread interrupt method and trigger the InterruptedException exception, which can be seen from the declarations of the two methods

8. ReentrantLock other common methods

isHeldByCurrentThread: instance method to judge whether the current thread holds the ReentrantLock lock lock. It has been used in the above code.

9. Comparison of four methods of obtaining locks

Method of obtaining lockRespond immediately (no blocking)Respond to interrupt
lock()××
lockInterruptibly()×
tryLock()×
tryLock(long timeout, TimeUnit unit)×

10. Summary

  1. ReentrantLock can realize fair lock and unfair lock
  2. ReentrantLock implements non fair locks by default
  3. The acquisition lock and release lock of ReentrantLock must appear in pairs. The locks must be released several times
  4. The operation of releasing the lock must be performed in finally
  5. The lockinterruptible () instance method can correspond to the interrupt method of the thread. When calling the interrupt() method of the thread, the lockinterruptible () method will trigger the InterruptedException exception
  6. About the InterruptedException exception, you can see that the method declaration has throws InterruptedException, which means that the method can interrupt the corresponding thread. When calling the thread's interrupt() method, these methods will trigger the InterruptedException exception. When the InterruptedException is triggered, the thread's interrupt state will be cleared. Therefore, if the program triggers an InterruptedException exception due to calling the interrupt () method, the thread flag changes from the default false to true, and then to false
  7. The instance method tryLock() will attempt to acquire the lock and return immediately. The return value indicates whether the acquisition is successful
  8. The instance method tryLock(long timeout, TimeUnit unit) will attempt to obtain the lock within the specified time. Whether the lock can be obtained within the specified time will be returned. The return value indicates whether the lock is obtained successfully. This method will respond to the interruption of the thread

Article reference: http://www.itsoku.com/ Bloggers think the content of this article is very good. If you are interested, you can learn about it.

Keywords: Java Back-end Multithreading JUC

Added by demon_athens on Thu, 25 Nov 2021 21:41:52 +0200