Basic use of Java lock and AQS

synchronized can solve the problem of Java concurrency. Why should jdk1.5 launch Java and provide multiple locks?

1. The difference between synchronized and Lock

Although synchronized and Lock can achieve synchronization, there are some differences between them.

  1. synchronized implicit acquisition lock is release lock. Lock needs to display acquisition and release lock, which has higher flexibility. However, if the lock is not released, it is easy to cause deadlock;
  2. synchronized if the Lock cannot be obtained, the thread will be blocked, but Lock provides a non blocking api, and the result can be returned immediately;
  3. synchronized is a synchronization keyword provided by Java, but the implementation classes are all under the Lock interface, which are Java classes;
  4. synchronized is non interruptible. Lock. Lockinterruptible() can respond to the interruption;
  5. synchronized is an unfair Lock, but Lock can provide both fair and unfair;

2. Basic use of Lock

Java also provides different implementation classes for locks. The most commonly used ones are ReentrantLock and ReentrantReadWriteLock. Here are reentrant lock and read-write lock.

2.1,ReentrantLock

ReentrantLock, as the name implies, is a lock that supports reentering. It means that the lock can support a thread to repeatedly lock resources. In addition, the lock also supports fair and unfair choice when acquiring the lock.

After using ReentrantLock for N times of lock, you also need n times of unlock(), otherwise other threads will not be able to get the lock, resulting in deadlock.

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * 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();
    }

Through the constructor, we can see that the fair lock and the unfair lock can be constructed by directly passing in the boolean. Fair lock uses FIFO principle. The thread with the longest waiting time in the waiting queue will seize the lock first. However, the efficiency of fair lock is not as high as that of non-public flat lock, because since strict FIFO is required, frequent thread context switching will occur, which will consume a lot of cpu resources when the cpu performs context switching on site. For unfair locks, the same thread is allowed to get the lock multiple times or not strictly follow FIFO, so the context switching of threads is not as frequent as that of fair locks.

Here is an example to understand the basic use of the next reentry lock:

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription: Reentrant lock
 * @Author: yangchenhui
 */
public class ReentrantLockDemo1 {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        lock.lock();  // block until condition holds
        try {
            System.out.println("Get lock for the first time");
            System.out.println("The number of times the current thread acquired the lock" + lock.getHoldCount());
            lock.lock();
            System.out.println("Get the lock for the second time");
            System.out.println("The number of times the current thread acquired the lock" + lock.getHoldCount());
        } finally {
            lock.unlock();
            lock.unlock();
        }
        System.out.println("The number of times the current thread acquired the lock" + lock.getHoldCount());

        // If it is not released, other threads will not get the lock at this time
        new Thread(() -> {
            System.out.println(Thread.currentThread() + " Expect to grab lock");
            lock.lock();
            System.out.println(Thread.currentThread() + " Thread got lock");
        }).start();

    }

}

The reentry lock can respond to thread interruption. The following is verified by a code example:

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription: Example of reentry lock response interrupt
 * @Author: yangchenhui
 */
public class ReentrantLockDemo2 {

    private Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo2 demo2 = new ReentrantLockDemo2();
        Runnable runnable = () -> {
            try {
                demo2.test(Thread.currentThread());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        // Wait 0.5 seconds for thread1 to execute first
        Thread.sleep(500);

        thread2.start();
        // Two seconds later, interrupt thread2
        Thread.sleep(2000);
        // If you don't use lock. Lockinterruptible(), the thread that interrupts will not respond, and the thread that obtains the lock will continue to execute
        thread2.interrupt();
    }

    public void test(Thread thread) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ", Want to get lock");
        //Note that if you need to properly interrupt the thread waiting for the lock, you must put the acquisition lock out, and then throw the InterruptedException
//        lock.lock();
        lock.lockInterruptibly();
        try {
            System.out.println(thread.getName() + "Got the lock.");
            // Grab the lock and don't release it for 10 seconds
            Thread.sleep(10000);
        } finally {
            System.out.println(Thread.currentThread().getName() + "implement finally");
            lock.unlock();
            System.out.println(thread.getName() + "Release the lock.");
        }
    }

}

The results are as follows:

After the response is interrupted, the code behind Thread-1 is not executed. If you use lock.lock() or synchronize key, you will not get this result.

Use tryLock() to return true if the lock can be obtained, and false immediately if the lock cannot be obtained.

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription:
 * @Author: yangchenhui
 */
public class ReentrantLockDemo3 {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            if (lock.tryLock()) {
                System.out.println("The current thread is" + Thread.currentThread().getName() + "Get to lock");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.unlock();
            System.out.println("The current thread is" + Thread.currentThread().getName() + "Release lock");
        };
        Thread thread1 = new Thread(runnable);
        thread1.start();
    }

}

2.1,ReentrantReadWriteLock

synchronized and ReentrantLock are exclusive locks. Only one thread is allowed to acquire the lock at the same time, but ReentrantReadWriteLock is a shared lock. Multiple threads are allowed to acquire the read lock at the same time.

To avoid dirty reads, ReentrantReadWriteLock provides a lock degradation mechanism: get write lock - > get read lock - > release write lock.

The read-write lock is a pair of mutually exclusive locks. If a thread currently holds the read lock, all threads (including the thread currently obtaining the unique read lock) cannot acquire the write lock. If the current thread acquires a write lock, it can acquire a write lock or a read lock again. It is based on this mechanism that the lock degradation mechanism of read-write lock can effectively avoid dirty reading.

The following uses an example to illustrate the basic use of read-write locks:

package com.xiaohuihui.lock.readwritelock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Desription: Read write lock example
 * @Author: yangchenhui
 */
public class ReentrantReadWriteLockDemo1 {

    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReentrantReadWriteLockDemo1 readWriteLock = new ReentrantReadWriteLockDemo1();
        // Multithreading read / write at the same time
        new Thread(() -> {
            readWriteLock.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLock.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLock.write(Thread.currentThread());
        }).start();
    }

    /**
     * Multithreaded read, shared lock
     * @param thread
     */
    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "Reading in progress");
            }
            System.out.println(thread.getName() + ""Read operation completed");
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    /**
     * write
     */
    public void write(Thread thread) {
        readWriteLock.writeLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "Writing in progress");
            }
            System.out.println(thread.getName() + ""Write operation completed");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

}

In AQS, the main operation of read lock is state, which can control the number of threads obtaining read lock by addition and subtraction. However, the operation of write lock is owner. If state=0 and owner=null, it means that no thread obtains read lock and no thread obtains write lock. Use CAS mechanism to modify the owner. If the modification is successful, it means that the lock is added successfully.

3. AQS mechanism

The queue synchronizer AbstractQueuedSynchronizer (hereinafter referred to as synchronizer) is the basic framework used to build locks or other synchronization components. It uses an int member variable to represent the synchronization state, and completes the queuing of resource acquisition threads through the built-in FIFO queue.

The synchronizer provides three methods (getState(), setState(int newState) and compareAndSetState(int expect, int update)) to operate, which can ensure that the state change is safe.

If we want to design a synchronization framework, first we need a status bit to represent the current lock status. Second, the thread participating in lock contention. If the lock is not acquired, we need to put it into a waiting queue. At the same time, in order to ensure the reentrancy, we need a field to store the current thread acquiring the lock. Following this idea, let's look at the class diagram in AQS.

In ReentrantLock, there is a Sync inner class to inherit AQS and use the method thread provided by the parent class to modify the lock state in the synchronizer.

Node node is used in AbstractQueuedSynchronizer to store the waiting thread collection. From the source code, we can see that it is a data structure of two-way linked list.

Exclusive owner thread is used in AbstractOwnableSynchronizer to store the lock owner of an exclusive lock.

When we call lock.lock(), we will call the compareAndSetState(int expect, int update) method in the synchronizer, and use unsafe to operate the state state. If the operation is successful, set the thread owner.

stateOffset is initialized in the static code block of AbstractQueuedSynchronizer. state is a volatile variable to maintain thread visibility.

In general, the AQS synchronization framework uses the cas operation of volatile + Unsafe to keep the state synchronized. At the same time, it uses the park() unpark() mechanism of LockSupport and the Node linked list to add and notify the waiting thread queue.

AQS provides template methods and hook functions, and provides a highly extensible framework for the construction of synchronization tools. This design pattern is worth learning.

The source code of unlock() is similar to lock(). You can follow it if you have time.

12 original articles published, 25 praised, 1651 visited
Private letter follow

Keywords: Java

Added by Gish on Mon, 17 Feb 2020 12:59:26 +0200