[comics] JAVA Concurrent Programming ReentrantLock of J.U.C Lock package

Keywords: Programming Java SDK jvm

Original statement: This article comes from the official account [fat pig programming]. Please indicate the source.

stay How to solve the atomicity problem in JAVA Concurrent Programming Finally, we sold a key. Mutex not only has synchronized keyword, but also can be implemented with Locks package in J.U.C. and it is very powerful! Let's find out today!


ReentrantLock

As the name implies, ReentrantLock is called reentrant lock. As the name implies, it means that a thread can repeatedly acquire the same lock.

ReentrantLock is also mutually exclusive, so atomicity can also be guaranteed.

Let's write a simple demo first. Let's take two threads in the atomic problem as an example. Now use ReentrantLock to rewrite:

    private void add10K() {
        // Acquire lock
        reentrantLock.lock();
        try {
            int idx = 0;
            while (idx++ < 10000) {
                count++;
            }
        } finally {
            // Ensure the lock can be released
            reentrantLock.unlock();
        }

    }

ReentrantLock can achieve the same effect as synchronized here. To facilitate your memory, I paste the code of synchronized to realize mutual exclusion again:

    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

The difference between ReentrantLock and synchronized

1. Reentry
synchronized can be re entered, because locking and unlocking are automatic, so it is not necessary to worry about whether to release the lock at last; ReentrantLock can also be re entered, but locking and unlocking need to be done manually, and the times need to be the same, otherwise other threads cannot obtain the lock.

2. Implementation
synchronized is implemented by the JVM, and ReentrantLock is implemented by the JDK. To put it bluntly, it's the operating system or the user's own code.

3. Performance
In Java version 1.5, synchronized performance is not as good as Lock in the SDK, but after version 1.6, synchronized has made many optimizations to catch up with the performance.

4. Function
The fineness and flexibility of ReentrantLock lock are obviously better than synchronized. After all, the more troublesome it is to use, the more functions it must have!

Special function 1: Fair lock or unfair lock can be specified, while synchronized can only be unfair lock.

Fairness means that the thread that waits first acquires the lock first. You can specify a fairness policy in the constructor.

    // Test the output to true and false, respectively. If true, the output order must be A B C, but if false, A C B may be output
    private static final ReentrantLock reentrantLock = new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo2 demo2 = new ReentrantLockDemo2();
        Thread a = new Thread(() -> { test(); }, "A");
        Thread b = new Thread(() -> { test(); }, "B");
        Thread c = new Thread(() -> { test(); }, "C");
        a.start();b.start();c.start();

    }
    public static void test() {
        reentrantLock.lock();
        try {
            System.out.println("thread " + Thread.currentThread().getName());
        } finally {
            reentrantLock.unlock();//Be sure to release the lock
        }
    }

At the end of the atomic article, we also sold a key. Taking transfer as an example, we explained that synchronized will lead to deadlock, that is, two threads you wait for my lock, I wait for your lock, both sides are blocked and will not release! For convenience, I post the code again:

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        // Lock transfer out account Thread1 locked A Thread2 locked B
        synchronized (source) {
            Thread.sleep(1000);
            log.info("Hold lock{} Wait for lock{}",source,target);
            // To lock the transfer in account Thread1, you need to obtain B, but it is locked by thread2. Thread2 needs to get A, but it is locked by Thread1. So wait for each other and lock up
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

ReentrantLock can avoid deadlock perfectly, because it can destroy one of the four necessary conditions of Deadlock: non preemptive condition. This is due to its several functions:

Special function 2: non blocking access to lock. If the attempt to acquire the lock fails, it does not enter the blocking state, but directly returns false. At this time, the thread can do other things first instead of blocking and waiting. So it will not cause deadlock.

// API supporting non blocking lock acquisition 
boolean tryLock();

Now let's use ReentrantLock to change the deadlock code

    static void transfer(Account source, Account target, int amt) throws InterruptedException {
        Boolean isContinue = true;
        while (isContinue) {
            if (source.getLock().tryLock()) {
                log.info("{}Lock acquired time{}", source.getLock(),System.currentTimeMillis());
                try {
                    if (target.getLock().tryLock()) {
                        log.info("{}Lock acquired time{}", target.getLock(),System.currentTimeMillis());
                        try {
                            log.info("Start transfer operation");
                            source.setBalance(source.getBalance() - amt);
                            target.setBalance(target.getBalance() + amt);
                            log.info("End transfer operation source{} target{}", source.getBalance(), target.getBalance());
                            isContinue=false;
                        } finally {
                            log.info("{}Release lock time{}", target.getLock(),System.currentTimeMillis());
                            target.getLock().unlock();
                        }
                    }
                } finally {
                    log.info("{}Release lock time{}", source.getLock(),System.currentTimeMillis());
                    source.getLock().unlock();
                }
            }
        }
    }

tryLock also supports timeouts. When tryLock is called, if the lock is not acquired, it will wait for a period of time. If the thread still fails to acquire the lock within a period of time, instead of entering the blocking state, it will throw interruptedexception, then the thread will also have the opportunity to release the lock once held, so as to break the non preemptive condition of necrotic lock.
boolean tryLock(long time, TimeUnit unit)

Special function 3: provide a mechanism to interrupt the thread waiting for the lock

The problem with synchronized is that if the attempt to acquire lock B fails after holding lock A, the thread will enter the blocking state. Once A deadlock occurs, there will be no chance to wake up the blocked thread.

But if the blocked thread can respond to the interrupt signal, that is to say, when we send the interrupt signal to the blocked thread, it can wake it up, then it has the opportunity to release the lock A that it once held. In this way, the conditions that cannot be preempted are destroyed. ReentrantLock can be implemented using the lockinterruptible method.

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo5 demo2 = new ReentrantLockDemo5();
        Thread th1 = new Thread(() -> {
            try {
                deadLock(reentrantLock1, reentrantLock2);
            } catch (InterruptedException e) {
                System.out.println("thread  A Interrupted");
            }
        }, "A");
        Thread th2 = new Thread(() -> {
            try {
                deadLock(reentrantLock2, reentrantLock1);
            } catch (InterruptedException e) {
                System.out.println("thread  B Interrupted");
            }
        }, "B");
        th1.start();
        th2.start();
        th1.interrupt();

    }


    public static void deadLock(Lock lock1, Lock lock2) throws InterruptedException {
        lock1.lockInterruptibly(); //If you use lock instead, it will be locked all the time
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock2.lockInterruptibly();
        try {
            System.out.println("Execution complete");
        } finally {
            lock1.unlock();
            lock2.unlock();
        }

    }

Special function 4. The Condition in J.U.C package can be used to realize the thread waiting for group wake-up. synchronized can only notify or notifyAll. This involves the collaboration between threads, which will be explained in detail in the official account.

How ReentrantLock ensures visibility

We just demonstrated that ReentrantLock guarantees atomicity. Does that guarantee visibility? The answer is a must.

Remember How to solve the problem of visibility and order in JAVA Concurrent Programming . We say that the visibility of multithreads in Java is guaranteed by the happens before rule. For example, the reason why synchronized can guarantee visibility is that there is a related synchronized rule: synchronized unlock happens before locks the lock later.

What does Lock in the Java SDK do to ensure visibility? The implementation of locks in the Java SDK is very complex, but the principle needs to be briefly introduced: it uses the relevant happens before rules of volatile.

The synchronization of ReentrantLock is actually delegated to AbstractQueuedSynchronizer. Locking and unlocking are achieved by changing the state property of AbstractQueuedSynchronizer, which is volatile.

When the lock is acquired, the value of state will be read and written; when the lock is unlocked, the value of state will also be read and written. The analogy of how volatile ensures visibility can solve this problem! If you don't know, you can review how to solve the problem of visibility and order in JAVA Concurrent Programming

summary

synchronized implements synchronous and mutually exclusive access to critical resources at the JVM level, but it has some large granularity and many limitations in dealing with practical problems, such as response interruption.

Lock provides a wider range of lock operations than synchronized, which can handle thread synchronization in a more elegant and flexible way.

We take ReentrantLock as an example to enter the world of Lock. The most important thing is to remember the unique functions of ReentrantLock, such as interrupt, timeout, non blocking Lock, etc. When your requirements meet these specific functions, you can only choose Lock instead of synchronized

Code in attachment github address: https://github.com/LYL41011/java-concurrency-learning

Original statement: This article comes from the official account [fat pig programming]. Please indicate the source.

Posted by chet23 on Tue, 12 May 2020 10:28:12 -0700