Synchronous control is an indispensable and important means for concurrent programs. The synchronized keyword is one of the simplest methods of control. Meanwhile, the wait() and notify() methods act as thread waits and notifications. These tools play an important role in implementing complex multithreaded collaboration. Synchronized, wait, notify, and reentrant locks are an alternative to synchronized, wait, and notify. This topic requires a basic understanding of synchronized, wait, and notify locks.

1 Synchronized function extension: reentrant lock

Reentrant locking is a perfect substitute for the synchronized keyword. In earlier JDK versions, reentrant locking performed much better than synchronized, and in later JDK versions, significant optimizations have been made to the synchronized keyword to achieve similar performance.

Here is a simple ReentrantLock use case:

public class ReenterLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i=0;

    public void run(a) {
        for(int j=0; j<10000000; j++){ lock.lock();// lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
// lock.unlock();}}}public static void main(String[] args) throws InterruptedException {
        ReenterLock r1 = new ReenterLock();
        Thread t1 = new Thread(r1);
        Thread t2 = newThread(r1); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}Copy the code

This code creates a global ReentrantLock object. This object is the ReentrantLock object. The area between the lock() and unlock() methods of this object is the ReentrantLock protection critical area, ensuring the safety of multithreading operations on the I variable.

As you can see from this code, reentrant locks have a display operation in contrast to synchronized. The developer must manually specify when to lock and when to release the lock. Because of this, reentrant logic control is much better than synchronized. However, it is worth noting that when exiting a critical section, you must remember to release the lock, otherwise you will never have a chance to access the critical section again, resulting in starvation and even deadlock of its threads.

A reentrant lock is called a reentrant lock because it can be entered repeatedly. Of course, repeated entries are limited to a single thread. An appeal code could also be written like this:

    public void run(a) {
        for(int j=0; j<10000000; j++){ lock.lock(); lock.lock();try {
                i++;
            }finally{ lock.unlock(); lock.unlock(); }}}Copy the code

In this case, a thread acquires the same lock twice in a row. It’s allowed! Note, however, that if a thread acquires a lock more than once, it must release the lock the same number of times. If release more times and then get a Java lang. IllegalMonitorStateException anomaly, on the other hand, if the release of income less, MAME is equivalent to the county also holds the lock, and as a result, other threads are unable to enter the critical section

In addition to the flexibility in use, reentrant provides some advanced functionality. For example, reentrant locks provide interrupt handling capabilities

1.1 Interrupt Response

In addition to the basic functions described above, reentrant locks provide some advanced features. For example, reentrant locks can provide interrupt handling capabilities. This is a very important feature; synchronized does not interrupt. While waiting for the lock, the program can cancel the lock request as needed. Synchronized can’t do that. In other words, the reentry lock has the ability to unlock deadlocks.

For example, the better you go to play with your friends, if you wait for half an hour and your friend doesn’t show up, you suddenly get a phone call saying that due to some unexpected situation, your friend can’t come as scheduled, then you must be disappointed to go home. Interrupts provide a similar mechanism. If a county is waiting for a lock, it can still receive a notification telling it to stop working without waiting, which is helpful in dealing with deadlocks.

The following code generates a deadlock, which can be resolved easily by breaking the lock:

public class IntLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public IntLock(int lock) {
        this.lock = lock;
    }

    public void run(a) {
        try {
            if (this.lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500); lock1.lockInterruptibly(); }}catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread())
                lock1.unlock();
            if (lock2.isHeldByCurrentThread())
                lock2.unlock();
            System.out.println(this.lock + "Thread exit"); }}public static void main(String[] args) throws InterruptedException {
        IntLock r1 = new IntLock(1);
        IntLock r2 = new IntLock(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000); t2.interrupt(); }}Copy the code

After threads T1 and T2 are started, t1 occupies lock1 first and then LOCK2. T2 occupies lock2 and then requests lock1. In this way, it is easy to form a deadlock between T1 and T2. Here, the lockInterruptibly() method is used uniformly for lock requests. This is a lock request action that responds to interrupts, that is, interrupts while waiting for the lock.

After the t1 and T2 threads start, the main thread sleeps at line 39, and the T1 and T2 threads are in deadlock state. Then the main thread interrupts the T2 thread at line 40, so t2 will give up the request to lock1 and release lock2 at the same time. This operation allows T1 to acquire lock2 and continue.

Executing the appeal code will print:

2Thread to exit1Thread to exit the Java. Lang. InterruptedException ats com. LXS. Demo. IntLock. Run (IntLock. Java:24)
	at java.lang.Thread.run(Thread.java:745)
Copy the code

As you can see, both threads exit after the interrupt. But it’s only T1 that really gets the job done. T2 abandons the task and exits directly, releasing resources.

1.2 Lock application waiting time limit

In addition to waiting for external notifications, another way to avoid deadlocks is to wait for a time limit. Often, a system cannot be automatically unlocked without knowing when a deadlock will occur. The best way to design a system is that it will not cause a deadlock at all. We can do a timed wait with the tryLock() method.

The following code shows the use of a timed wait lock:

public class TimeLock implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();

    public void run(a) {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("Get lock failed"); }}catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(lock.isHeldByCurrentThread()) lock.unlock(); }}public static void main(String [] args){
        TimeLock lock1 = new TimeLock();
        Thread t1 = new Thread(lock1);
        Thread t2 = newThread(lock1); t1.start(); t2.start(); }}Copy the code

Output result:

Get lock failed
Copy the code

Here, tryLock() takes two arguments, one for the wait time and one for the unit of time. The value is set to seconds, and the duration is 5, indicating that the thread waits at most 5 seconds on the lock request. Returns false if the lock has not been acquired for more than 5 seconds. Returns true on success. In this case, because the thread holding the lock holds it for six seconds, another thread cannot acquire the lock for five seconds, and therefore, the request for the lock fails.

The tryLock() method can also be run without arguments, in which case the current process attempts to acquire the lock, and if the lock is not occupied by another process, the request is successful, returning true immediately. If the lock is occupied by another thread, false is immediately returned. This mode does not cause threads to wait and therefore does not cause deadlocks. This is illustrated below:

public class TryLock implements Runnable {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;

    public TryLock(int lock) {
        this.lock = lock;
    }

    public void run(a) {
        if (lock == 1) {
            while (true) {
                if(lock1.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock2.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally{ lock2.unlock(); }}}finally{ lock1.unlock(); }}}}else {
            while (true) {
                if(lock2.tryLock()){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (lock1.tryLock()) {
                            try {
                                System.out.println(Thread.currentThread().getId() + " my job done");
                                return;
                            } finally{ lock1.unlock(); }}}finally {
                        lock2.unlock();
                    }
                }
            }
        }
    }

    public static void main(String [] args){
        TryLock lock1 = new TryLock(1);
        TryLock lock2 = new TryLock(2);
        Thread t1 = new Thread(lock1);
        Thread t2 = newThread(lock2); t1.start(); t2.start(); }}Copy the code

In the above code, a very deadlock-prone locking sequence is used. So t1 is asking for lock1, and t2 is asking for lock2, and t2 is requesting lock1. In general, this would cause T1 and T2 to wait for each other, causing a deadlock. With tryLock, however, the situation has improved. Because a thread does not wait foolishly, but keeps trying, as long as it executes long enough, the thread will always get the resources it needs to execute properly.

The code execution result is as follows:

12 my job done
11 my job done
Copy the code

1.3 fair lock

In most cases, lock claims are unfair. That is, thread 1 first requests lock A, and thread 2 then requests lock A. So when lock A is available, can thread 1 acquire the lock or can thread 2 acquire the lock? This is not certain, the system will only randomly pick a lock from the wait queue. So there is no guarantee of fairness. And the next to talk about fair lock, he will be in accordance with the order of time, to ensure that first come first served, last come last. Therefore, the biggest characteristic of fair lock is that he will not produce hunger phenomenon. Note: If a thread is mutually exclusive with synchronized, the resulting lock is unfair. The reentrant lock allows us to set fairness. It has the following constructor:

public ReentranLock(boolean fair);
Copy the code

When fair is true, the lock is fair. Fair lock looks beautiful, but in order to achieve fair lock, the system must maintain an ordered queue, so the implementation cost of fair lock is relatively high, which means the efficiency of fair lock is very low. Therefore, by default, the lock is not fair. Try not to use fair locks unless you have specific requirements.

The following code highlights the characteristics of fair locking:

public class FairLock implements Runnable {
    public static ReentrantLock fairLock = new ReentrantLock(true);
//    public static ReentrantLock fairLock = new ReentrantLock();

    public void run() {
        while (true){
            try {
                fairLock.lock();
                System.out.println(Thread.currentThread().getName());
            }finally {
                fairLock.unlock();
            }
        }
    }

    public static void main(String [] args){
        FairLock r1 = new FairLock();
        Thread t1 = new Thread(r1,"Thread_t1");
        Thread t2 = new Thread(r1,"Thread_t2"); t1.start(); t2.start(); }}Copy the code

Code execution result:

Thread_t1
Thread_t2
Thread_t1
Thread_t2
Thread_t1
Thread_t2
Copy the code

As you can see, thread scheduling is fair.

The methods for ReentantLock above are summarized as follows

  • Lock () : Obtains the lock and waits if it is already occupied.
  • LockInterruptibly () : Obtains the lock, but preferentially responds to interrupts.
  • TryLock () : Attempts to acquire the lock, returning true on success and false on failure. The method does not wait and returns immediately
  • TryLock (long time, TimeUnit unit) : Attempts to acquire the lock within a given time.
  • Unlock () : Releases the lock.

A good partner for reentrant locks: Condition

Condition objects are easy to understand if you understand object.wait() and object.notify(). This is basically the same as wait() and notify(). But wait() and notify() are used in combination with the synchronized keyword, while condition is associated with a reentrant lock. The basic methods provided by the Condition interface are as follows:

void await(a) throws InterrupteException;
void awaitUninterruptibly(a);
long awaitNanos(long nanosTimeout) throws InterrupteException;
boolean await(long time, TimeUnit unit) throws InterrupteException;
boolean awaitUntil(Data deadline) throws InterrupteException;
void signal(a);
void signalAll(a);
Copy the code

The meanings of the above methods are as follows:

  • The await() method causes the current thread to wait while the current lock is released, and when another thread uses signal() or signalAll(), the thread regains the lock and continues execution. Or when the thread is interrupted, it can jump out of the wait. This is similar to the object.wait() method.

  • The awaitUninterruptibly() method is the same as wait() except that it does not respond to interrupts while waiting.

  • The signal() method is used to wake up a waiting thread. SignalAll () wakes up all waiting threads. This is similar to the object.notify() method.

The following code briefly demonstrates Condition in action:

public class ReenterLockCondition implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public void run(a) {
        try{
            lock.lock();
            condition.await();
            System.out.println("Thread is going on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{ lock.unlock(); }}public static void main(String [] args) throws InterruptedException {
        ReenterLockCondition r1 = new ReenterLockCondition();
        Thread t1= new Thread(r1);
        t1.start();
        Thread.sleep(2000); lock.lock(); condition.signal(); lock.unlock(); }}Copy the code

Line 3 begins by generating a condition object bound to lock. Line 8 requires the thread to wait on the condition object. The main thread sends a signal two seconds later telling the threads waiting on condition that they can continue.

As with Object.wait () and Object.notify (), when a thread uses condition.wait(), it is required to hold the associated reentrant lock, which the thread releases voluntarily after condition.wait() is called. Also, the thread is required to acquire the associated lock when the condition.signal() method is called. Note that after the signal() method call, it is important to release the associated lock on line 24, giving the lock to another thread. If 24 lines were omitted, county T1 would have been awakened, but the lock would not be reacquired, because execution would not actually continue.

3 allow multiple threads to access simultaneously: Semaphore

Semaphores provide a more powerful way to control multithreaded collaboration. Broadly speaking, a semaphore is an extension of a lock. Synchronized and ReentranLock allow only one thread to access a resource at a time, whereas semaphore can specify multiple threads to access a resource at the same time. The semaphore provides the following constructors:

public Semaphore(int permits);

public Semaphore(int permits, boolean fair); // The second argument can specify whether it is fair
Copy the code

When constructing a semaphore, you must specify the number of permissions that can be applied for at the same time. When only one permission is requested per thread, this specifies how many threads can access a resource at the same time. The main logical methods of semaphore are:

public void acquire(a);

public void acquireUninterruptibly(a);

public boolean tryAcquire(a);

public boolean tryAcquire(long timeout, TimeUnit unit);

public void release(a);
Copy the code

The acquire() method attempts to obtain permission for an access. If not, the thread waits until permission is requested or the current thread is interrupted. The acquireUninterruptibly() method is similar to acquire() but does not respond to interrupts. TryAcquire () attempts to acquire a license, returns true on success and false on failure, it does not block wait and returns immediately. Release () is used to release a license after a thread has finished accessing the resource so that other threads waiting for permission can access the resource.

Here’s a simple use of Semaphore:

public class SemapDemo implements Runnable {
    // A group of 5 outputs
    final Semaphore semp = new Semaphore(5);

    public void run(a) {
        try {
            semp.acquire();
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getId() + " done!");
            semp.release();
        } catch(InterruptedException e) { e.printStackTrace(); }}public static void main(String []args){
        ExecutorService exec = Executors.newFixedThreadPool(20);
        final SemapDemo demo = new SemapDemo();
        for(int i=0; i<20; i++){ exec.submit(demo); }}}Copy the code

In this example, 20 threads are started simultaneously. If you look at the output of the above program, you will see that the threads are output in groups of five.

4 ReadWriteLock Read/write lock

ReadWriteLock is a read/write separation lock provided in JDK5. Read/write locks can effectively reduce lock contention and improve system performance. For example, county A1, A2, and A3 carry out write operations, while threads B1, B2, and B3 carry out read operations. If a reentrenter lock or internal lock is used, all reads, reads, and writes are serial operations in theory. When B1 reads, B2 and B3 wait for locks. Since the read operation does not damage the integrity of the data, this wait is obviously unreasonable. Therefore, the reading and writing institute uses the scope of play.

In this case, read/write allows multiple threads to read simultaneously, in parallel between B1, B2, and B3. However, for data integrity, write, and read/write operations still need to wait on each other and hold locks. In general, read/write locks restrict access as shown in the following table

read write
read non-blocking blocking
write blocking blocking
  • Read – Read not mutually exclusive: There is no blocking between reads.

  • Read – write mutex: Read blocks write, and write blocks read.

  • Write – write mutex: Write blocks.

If the number of read operations is much greater than that of write operations, read/write locks maximize system performance. Here is a slightly exaggerated example of how read-write locks can help performance.

public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            System.out.println("read success");
            return value;
        } finally{ lock.unlock(); }}public void handleWrite(Lock lock, int index) throws InterruptedException {
        try {
            lock.lock();
            Thread.sleep(1000);
            value = index;
            System.out.println("write success");
        } finally{ lock.unlock(); }}public static void main(String[] args) {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            public void run(a) {
                try {
                    demo.handleRead(readLock);
// demo.handleRead(lock);
                } catch(InterruptedException e) { e.printStackTrace(); }}}; Runnable writeRunnable =new Runnable() {

            public void run(a) {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
// demo.handleWrite(lock, new Random().nextInt());
                } catch(InterruptedException e) { e.printStackTrace(); }}};for (int i = 0; i < 18; i++) {
            new Thread(readRunnable).start();
        }

        for (int i = 18; i < 20; i++) {
            newThread(writeRunnable).start(); }}}Copy the code

In the above code, the system takes much more time to complete read and write tasks when using read and write locks than when using normal locks

From the execution result, you can see that the program takes 20 seconds to complete the read and write task without the read and write lock. Using read and write lock, the program needs 3 seconds to complete the read and write task.

Counter: CoundownLatch

CountDownlatch is a very useful multithreaded control tool class, simply referred to as the countdown counter. This tool is commonly used to control thread waits, it can make a thread wait until the count ends, before execution begins

A typical scenario is a rocket launch. Before a rocket launch, the engine will be fired and executed after multiple checks are done to ensure that nothing goes wrong. This scenario is perfect for CountDownLatch, which is the firing thread waiting for all the check threads to complete.

The CountDownLatch constructor takes an integer as an argument, the current count of the counter.

    public CountDownLatch(int count)
Copy the code

The following example demonstrates the method used by CountDownLatch

public class CountDownLatchDemo implements Runnable {
    static final CountDownLatch end = new CountDownLatch(10);
    static final CountDownLatchDemo demo = new CountDownLatchDemo();


    public void run(a) {
        try {
            Thread.sleep(new Random().nextInt(10) * 1000);
            System.out.println("check complete!");
            end.countDown();
        } catch(InterruptedException e) { e.printStackTrace(); }}public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            exec.submit(demo);
        }
        end.await();
        System.out.println("Fire!"); exec.shutdown(); }}Copy the code

Line 2 of the code above generates an instance of CountDownLatch with a count of 10, which means that 10 threads are required to complete the task and wait for the threads on CountDownLatch to continue. Line 10 uses the countdownlatch.countdown () method to notify CountDownLatch that a thread has completed its task until the counter is reduced by 1. Line 21 uses the countdownlatch.await () method to require the main thread to wait for all check tasks to complete and for all 10 tasks to complete before it can continue.

The above case execution logic is simply shown in the following figure

The main thread waits on CountDownLatch until all check thread tasks are complete

Thread blocking utility class: LockSupport

Start with an example of the suspend() method that freezes a thread

public class BadSuspend {
    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread{
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run(a) {
            synchronized (u){
                System.out.println("in "+ getName()); Thread.currentThread().suspend(); }}}// The order in which resume does not take effect might be:
    // Print T1 => T1. Suspend => T2. Wait for U release
    public static void main(String []args) throws InterruptedException {
        t1.start();
        Thread.sleep(100); t2.start(); t1.resume(); t2.resume(); t1.join(); t2.join(); }}Copy the code

Schematic diagram of the above case execution

The main function calls the resume() method, but because of the chronological order, the resume does not take effect! This results in thread T2 being permanently suspended and holding the lock on object U forever, which can be fatal to the system.

LockSupport is a handy thread-blocking tool that can block threads anywhere within a thread. In contrast to the thread.suspend () method, she makes up for cases where a Thread cannot continue because the resume method occurs. Unlike the object.wait () method, it does not need to acquire the lock on an Object and does not throw InterruptedExeption.

Now rewrite the program using LockSupport

public class LockSupportDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run(a) {
            synchronized (u) {
                System.out.println("in "+ getName()); LockSupport.park(); }}}public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100); t2.start(); LockSupport.unpark(t1); LockSupport.unpark(t2); t1.join(); t2.join(); }}Copy the code

This case can end without the thread being permanently suspended because of the Pack () method. This is because the LockSupport class uses a semaphore like mechanism, which prepares a license for each thread. If the license is available, pack() returns immediately. And consuming the license (that is, making the license unavailable) blocks if the license is unavailable, whereas unpack makes a license available, which allows the next pack() method to execute and return immediately, even if the unpack occurs before the Pack method.

The locksupport.pack () method also supports interrupt effects, but unlike other interrupt accepting functions, the locksupport.pack () method does not throw InterruptedException. It will only return in silence, but we can get the interrupt flag from methods like Thread.interrupted()

public class LockSupportIntDemo {

    public static Object u = new Object();
    static ChangeObjectThread t1 = new ChangeObjectThread("t1");
    static ChangeObjectThread t2 = new ChangeObjectThread("t2");

    public static class ChangeObjectThread extends Thread {
        public ChangeObjectThread(String name) {
            super.setName(name);
        }


        public void run(a) {
            synchronized (u) {
                System.out.println("in " + getName());
                LockSupport.park();
                if (Thread.interrupted()) {
                    System.out.println(getName() + "Interrupted.");
                }
            }
            System.out.println(getName() + "End of execution"); }}public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100); t2.start(); t1.interrupt(); LockSupport.unpark(t2); }}Copy the code

Line 29 interrupts T1 in the pack() state, after which T1 can immediately respond to the interrupt and return. After t1 returns, t2 waiting outside can enter the critical section and is finally terminated by locksupport.unpack (T2).