Author: Tangyuan

Personal blog: Javalover.cc

preface

In the concurrency opening, we introduced synchronized;

In this section we’ll look at explicit locks

Explicit locks include ReentrantLock and ReadWriteLock

The relationship is as follows:

Introduction to the

The biggest difference between an explicit lock and a built-in lock is that the explicit lock requires manual lock acquisition and lock release, while the built-in lock does not

For explicit locks, this section describes its implementation class, reentrant locks, and its related class, read-write locks

  • Reentrant lock, implements explicit lock, meaning reentrant explicit lock (built-in lock is also reentrant)

  • Read/write lock, the explicit lock is divided into read/write separation, that is, read can be parallel, multiple threads reading at the same time will not block (read/write, write or serial)

So let’s get started

If there is any problem with the article, you are welcome to criticize and correct it. Thank you

directory

  1. ReentrantLock
  2. Read-write lock ReadWriteLock
  3. The difference between

The body of the

1. ReentrantLock

Let’s take a look at some of its methods:

  • public ReentrantLock(); Constructor, which by default constructs an unfair lock (queue-jumping). If a thread obtains the lock just as the lock is released, the thread obtains the lock immediately, regardless of whether the thread in the queue is waiting.)

  • Public void lock() : Acquires the lock and blocks it (if another thread holds the lock, blocks the current thread until the lock is released);

  • Public void lockInterruptibly() throws InterruptedException: Obtains the lock in a manner that can be interrupted (throws an interrupt exception if the current thread is interrupted);

  • Public Boolean tryLock(): Try to obtain the lock, return false immediately if the lock is held by another thread

  • Public Boolean tryLock(long timeout, TimeUnit Unit) throws InterruptedException: Try to obtain the lock and set a timeout period. Return false)

  • Public void unlock(): Releases the lock

First, let’s take a look at its construction method, internal implementation is as follows:

public ReentrantLock(a) {
  sync = new NonfairSync();
}
Copy the code

As you can see, an unfair lock has been created

Fair lock: If the lock is held by another thread, the current thread is queued

Unfair lock: If a lock is acquired just as the lock is released, the thread acquires the lock immediately, regardless of whether the thread in the queue is waiting

The advantage of unfair locking is that it reduces the hang and wake up overhead of the thread

The advantage of unfair locking is obvious if the execution time of a particular thread is very short, even less than the time it takes to wake up the thread in the queue

We can assume the following scenario:

  • The task execution time of thread A is 10ms
  • The time between waking up thread B in the queue and actually executing the task of thread B is 20ms
  • So when thread A goes to acquire the lock, the lock is released again, thread A takes the lock first, performs the task, and then releases the lock
  • When thread A releases the lock and thread B wakes up in the queue to acquire the lock, the CPU is not wasted while thread B wakes up, thus improving program performance

This is why the default is unfair (generally, unfair locks perform better than fair locks)

So when should you use a fair lock?

  • The lock is held for a long time, that is, the task execution of the thread takes a long time
  • The interval between lock requests is long

Because in this case, if a thread cuts to the queue to acquire the lock, the task will not complete for half a day. Then the thread that is awakened in the queue will wake up and find that the lock is still occupied, and will be placed in the queue again (this does not improve performance, but may decrease).

Now let’s look at the key part: getting the lock

There are several ways to obtain a lock. Let’s look at the differences between them in code

  1. Take a look at the lock() method, which looks like this:
public class ReentrantLockDemo {

    private Lock lock = new ReentrantLock();

    private int i = 0;

    public void add(a){
        lock.lock();
        try {
            i++;
        }finally{ System.out.println(i); lock.unlock(); }}public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 100; i++) { service.submit(()->{ demo.add(); }); }}}Copy the code

Output 1 to 100 because lock() blocks when it acquires the lock

  1. Now look at the tryLock() method, which looks like this:
public class ReentrantLockDemo {

    private Lock lock = new ReentrantLock();

    private int i = 0;

    public void tryAdd(a){
        if(lock.tryLock()){
            try {
                i++;
            }finally{ System.out.println(i); lock.unlock(); }}}public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 100; i++) { service.submit(()->{ demo.tryAdd(); }); }}}Copy the code

The run found that the output is always less than 100 because tryLock() returns false immediately if it fails to acquire the lock, rather than blocking and waiting

  1. Finally, take a look at the lockInterruptibly() method, which also blocks to acquire the lock, but with an interrupt exception that is thrown if the thread is interrupted while acquiring the lock
public class ReentrantLockDemo {

    private Lock lock = new ReentrantLock();

    private int i = 0;

    public void interruptAdd(a){
        try {
            lock.lockInterruptibly();
            i++;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally{ System.out.println(i); lock.unlock(); }}public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        ExecutorService service = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 100; i++) {
						For the 10th time, immediately close the thread pool and stop all threads (both executing and waiting).
            if (10== i){ service.shutdownNow(); } service.submit(()->{ demo.interruptAdd(); }); }}}Copy the code

Run a few more times and you might get the following output:

1
2
3
4
5
6
6
6
6
6
java.lang.InterruptedException
	at 
......
Copy the code

This is because the first few threads normally acquired the lock and executed i++, but the next few threads were suddenly stopped and threw an interrupt exception

  1. And then finally release the lock, unlock()

This is very simple, the above code has to do with this release lock

You may have noticed, however, that unlock() is written ina finally block

This is because it is possible to throw an exception when acquiring a lock and executing a task. If you do not place unlock() ina finally block, the lock will not be released. This is a big problem later on.

This is one of the reasons explicit locks are not a complete substitute for built-in locks

2. Read/write lock ReadWriteLock

Inside the read-write lock are two methods that return the read lock and the write lock respectively

Read locks are shared locks, while write locks are exclusive locks (as are the previously described reentrant locks and built-in locks)

Read locks allow multiple threads to acquire a lock at the same time, and because reads do not modify data, they are ideal for situations where reads are excessive and writes are minimal

So let’s look at it in code

Let’s take a look at the read lock. The code is as follows:

public class ReadWriteLockDemo {

    private int i = 0;
    private Lock readLock;
    private Lock writeLock;


    public ReadWriteLockDemo(a) {
        ReadWriteLock lock = new ReentrantReadWriteLock();
        this.readLock = lock.readLock();
        this.writeLock = lock.writeLock();
    }

    public void readFun(a){
        readLock.lock();
        System.out.println("=== get read lock ===");
        try {
            System.out.println(i);
        }finally {
            readLock.unlock();
            System.out.println("=== release read lock ==="); }}public static void main(String[] args) throws InterruptedException {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();
        ExecutorService executors = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) { executors.submit(()->{ demo.readFun(); }); }}}Copy the code

Run multiple times, and it is possible to print the following:

=== Obtain the read lock ===0=== Obtain the read lock ===Copy the code

As you can see, both threads have acquired the read lock. This is the advantage of the read lock. Multiple threads read simultaneously

ReentrantReadWriteLock class ReentrantReadWriteLock

public class ReadWriteLockDemo {

    private int i = 0;
    private Lock readLock;
    private Lock writeLock;

    public ReadWriteLockDemo(a) {
        ReadWriteLock lock = new ReentrantReadWriteLock();
        this.readLock = lock.readLock();
        this.writeLock = lock.writeLock();
    }

    public void writeFun(a){
        writeLock.lock();
        System.out.println("=== get write lock ===");
        try {
            i++;
            System.out.println(i);
        }finally {
            writeLock.unlock();
            System.out.println("=== release write lock ==="); }}public static void main(String[] args) throws InterruptedException {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();
        ExecutorService executors = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10; i++) { executors.submit(()->{ demo.writeFun(); }); }}}Copy the code

As you can see, the write lock is similar to the lock() method of the reentrant lock above, blocking to acquire the write lock

=== Obtain write lock ===1=== Release the write lock ====== Obtain the write lock ===2=== Release the write lock ====== Obtain the write lock ===3=== Release the write lock ====== Obtain the write lock ===4=== Release the write lock ====== Obtain the write lock ===5=== Release the write lock ====== Obtain the write lock ===6=== Release the write lock ====== Obtain the write lock ===7=== Release the write lock ====== Obtain the write lock ===8=== Release the write lock ====== Obtain the write lock ===9=== Release the write lock ====== Obtain the write lock ===10=== release write lock ===Copy the code

One thing to note about read-write locks is that read and write locks must be based on the same ReadWriteLock class to make sense

If the read Lock and the write Lock are obtained from two separate ReadWrite Lock classes, then the read Lock and the write Lock are completely separate and will not act as locks (preventing other threads from accessing them)

This is similar to synchronized(a) and synchronized(b), which lock two objects and can be accessed by a single thread

3. The difference between

Let’s use the table to show it, the details are as follows:

The characteristics of the lock Built-in lock Reentrant lock Read-write lock
flexibility low high high
fairness Not sure Unfair (default) + fair Unfair (default) + fair
Regular sex There is no Can time Can time
interruptible There is no interruptible interruptible
Mutual exclusivity The mutex The mutex Read Shared. Everything else is mutually exclusive

It is recommended that built-in locks be preferred. Explicit locks are used only when the built-in locks cannot meet the requirements (such as timing, interruption, and fairness).

Read/write locks are recommended for scenarios (such as configuration data) in which read/write is excessive

conclusion

  1. ReentrantLock: To explicitly acquire and release the lock, remember to release the lock ina finally block
  2. Read/write lock ReadWriteLock: Based on explicit lock (it has all kinds of explicit locks), with read/write separation, implements read sharing (multiple threads read at the same time), nothing else is shared (read/write, write).
  3. Differences: Built-in locks do not support manual lock acquisition/release, fair selection, timing, interrupt, explicit locks support

It is recommended to use built-in locks first

Because now the performance of the built-in lock is not much different from that of the explicit lock

And explicit locks run the risk of forgetting to release because they need to be released manually (ina finally block)

Read/write locks are recommended for read/write situations. (Pairs of read and write locks must be obtained from the same read/write lock class.)

Reference content:

  • Java Concurrent Programming
  • Real Java High Concurrency

Afterword.

Finally, I wish you all the best and a happy family