Wait for notification mode with lock

Posted by Ruski on Thu, 17 Feb 2022 09:20:26 +0100

Using lock to realize waiting and notification mode

Keyword synchronized, class ReentrantLock to realize waiting and notification mode

ReentrantLock class

In Java multithreading, the synchronized keyword can be used to realize the synchronization and mutual exclusion between threads, but in jdk1 The newly added ReentrantLock class in 5 can achieve the same effect, and it is also more powerful in extended functions, such as sniffing locking, multi-channel branch notification and other functions, and it is more flexible than synchronized in use.

public class MyService {

    private Lock lock = new ReentrantLock();

    public void methodA() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " methodA begin:" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " methodA end:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void methodB() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " methodB begin:" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " methodB end:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
	}
 }

    public static void main(String[] args) {
        MyService myService = new MyService();

        ThreadB a = new ThreadB(myService);
        a.setName("A");
        a.start();

        ThreadA b = new ThreadA(myService);
        b.setName("B");
        b.start();

    }

Output after running the above program;

A methodB begin:1620395826076
A methodB end:1620395831084
B methodA begin:1620395831084
B methodA end:1620395836091

This experiment shows that call lock The thread of lock () code holds the "object monitor", and other threads can only compete again when the thread lock is released.

The effect is the same as using the synchronized keyword. Threads are executed sequentially.

Also note the following examples:

In a way of using blocking wait to acquire a lock, you must try Outside the code block, and in the locking method and try There are no method calls that may throw exceptions between code blocks to avoid locking successfully finally Cannot unlock in.
    
Note 1: if lock Method and try If the method call between code blocks throws an exception, it cannot be unlocked, causing other threads to fail to obtain the lock.
Note 2: if lock Method in try Within the code block, exceptions may be thrown by other methods, resulting in finally In the code block, unlock When an unlocked object is unlocked, it calls AQS of tryRelease Method (depending on the implementation class), throw IllegalMonitorStateException Abnormal.
Note 3: in Lock Object lock Method implementation may throw unchecked If it is abnormal, the consequences are the same as those in instruction 2. java.concurrent.LockShouldWithTryFinallyRule.rule.desc
    
Positive example: 
    Lock lock = new XxxLock();
    // ...
    lock.lock();
    try {
        doSomething();
        doOthers();
    } finally {
        lock.unlock();
    }
Negative example: 
    Lock lock = new XxxLock();
    // ...
    try {
        // If an exception is thrown here, the finally block is executed directly
        doSomething();
        // The finally block executes regardless of whether the lock is successful or not
        lock.lock();
        doOthers();
    } finally {
        lock.unlock();
    }

ReentrantLock class uses Condition to realize waiting and notification mode

Class ReentrantLock can realize waiting and notification modes with the help of Condition objects.

Condition class is a technology that appears in JDK5. Using it has better flexibility. For example, it can realize multi-channel notification function, that is, multiple condition (i.e. object monitor) instances can be created in a Lock object, and thread objects can be registered in the specified conditions, so as to selectively notify threads and be more flexible in scheduling threads.

The thread selective notification function of Condition is very important, and it is provided by default in the Condition class.

    private Condition condition = lock.newCondition();

    // As noted in the example above, the following method throws a monitor error,
    // The solution must be in condition Await () method before calling lock. The lock () code gets the synchronization monitor
    public void await() {
        try {
            // todo lock.lock();
            lock.lock();
            System.out.println("await before");
            condition.await();
            System.out.println("await after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

Program running results:

await before

The reason is that the await() method of the Condition object is called, so that the thread currently executing the task enters the Waiting state.

Correctly use Condition to realize waiting and notification

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

/**
 * Use Condition correctly to realize waiting / notification
 *
 * @author xuz
 * @version 1.0
 * @date 2021/5/7 22:24
 */
public class MyService {
    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void await() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " await time:" + System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " signal time:" + System.currentTimeMillis());
            //Wakes up one waiting thread.
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

}

    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA(myService);
        a.start();
        Thread.sleep(3000);
        myService.signal();
    }

Program output:
//Thread-0 in awaiting was awakened by the main thread
Thread-0 await time:1620438361597
main signal time:1620438364607

Wait / notify mode successfully implemented

The wait() method in the Object class is equivalent to the await() method in the Condition class.

The wait(long timeout) method in the Object class is equivalent to the await (long time, timeunit) method in the Condition class.

The notify() method in the Object class is equivalent to the signal() method in the Condition class.

The notifyAll() method in the Object class is equivalent to the signalAll() method in the Condition class.

Using multiple conditions to realize the notification part of the thread

What is the difference between one Condition object and multiple Condition objects?

public class MyService {
    private Lock lock = new ReentrantLock();

    private Condition condition = lock.newCondition();

    public void awaitA() {
        try {
            lock.lock();
            System.out.println("begin awaitA Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            condition.await();
            System.out.println("end awaitA Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void awaitB() {
        try {
            lock.lock();
            System.out.println("begin awaitB Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            condition.await();
            System.out.println("end awaitB Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void signalAll() {
        try {
            lock.lock();
            System.out.println("signalAll Time is " + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            //Wakes up multi waiting thread.
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

}

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA(myService);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(myService);
        b.setName("B");
        b.start();
        Thread.sleep(3000);
        myService.signalAll();
    }
}

Program run output:

//As you can see, using A Condition object, threads A and B are awakened

begin awaitA time: 1620549030981 ThreadName=A
begin awaitB time: 1620549030982 ThreadName=B
signalAll time is 1620549033989 ThreadName=main
end awaitA time: 1620549033989 ThreadName=A
end awaitB time: 1620549033989 ThreadName=B

What should I do if I want to wake up some threads separately? At this time, it is necessary to use multiple Condition objects,

The Condition object can wake up some specified threads, which helps to improve the running efficiency of the program You can group threads first and then wake up the threads in the specified group

Using multiple conditions to implement the notification part of the thread

public class MyService {
    private Lock lock = new ReentrantLock();

    private Condition conditionA = lock.newCondition();

    private Condition conditionB = lock.newCondition();

    public void awaitA() {
        try {
            lock.lock();
            System.out.println("begin awaitA Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            conditionA.await();
            System.out.println("end awaitA Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void awaitB() {
        try {
            lock.lock();
            System.out.println("begin awaitB Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            conditionB.await();
            System.out.println("end awaitB Time is:" + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public void signalAll_A() {
        try {
            lock.lock();
            System.out.println("signalAll_A Time is " + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            //Wakes up multi waiting thread.
            conditionA.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_B() {
        try {
            lock.lock();
            System.out.println("signalAll_B Time is " + System.currentTimeMillis() + " ThreadName=" + Thread.currentThread().getName());
            //Wakes up multi waiting thread.
            conditionB.signalAll();
        } finally {
            lock.unlock();
        }
    }

}

public class Run {
    public static void main(String[] args) throws InterruptedException {
        MyService myService = new MyService();
        ThreadA a = new ThreadA(myService);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(myService);
        b.setName("B");
        b.start();
        Thread.sleep(3000);
        myService.signalAll_A();
    }
}

Program output:

//Through this experiment, we know that using ReentrantLock object can wake up threads of a specified type, which is a convenient way to control the behavior of some threads

begin awaitA time: 1620548870707 ThreadName=A
begin awaitB time: 1620548870708 ThreadName=B
signalAll_A time is 1620548873714 ThreadName=main
end awaitA time: 1620548873714 ThreadName=A

ArrayBlockingQueue multiple conditions to notify some threads

Now let's learn about Java util. concurrent. The arrayblockingqueue source code uses ReentrantLock and Condition to realize the waiting and notification mode

  /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }    

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

    /**
     * Inserts element at current put position, advances, and signals.
     * Call only when holding lock.
     */
    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

   /**
     * Inserts the specified element at the tail of this queue if it is
     * possible to do so immediately without exceeding the queue's capacity,
     * returning {@code true} upon success and {@code false} if this queue
     * is full.  This method is generally preferable to method {@link #add},
     * which can fail to insert an element only by throwing an exception.
     *
     * @throws NullPointerException if the specified element is null
     */
    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Inserts the specified element at the tail of this queue, waiting
     * for space to become available if the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Inserts the specified element at the tail of this queue, waiting
     * up to the specified wait time for space to become available if
     * the queue is full.
     *
     * @throws InterruptedException {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

WorkQueue in kubernetes information mechanism

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
	Lock()
	Unlock()
}


func newQueue(c clock.Clock, metrics queueMetrics, updatePeriod time.Duration) *Type {
	t := &Type{
		clock:                      c,
		dirty:                      set{},
		processing:                 set{},
		cond:                       sync.NewCond(&sync.Mutex{}),
		metrics:                    metrics,
		unfinishedWorkUpdatePeriod: updatePeriod,
	}
	go t.updateUnfinishedWorkLoop()
	return t
}

// Type is a work queue (see the package comment).
type Type struct {
	// queue defines the order in which we will work on items. Every
	// element of queue should be in the dirty set and not in the
	// processing set.
	queue []t

	// dirty defines all of the items that need to be processed.
	dirty set

	// Things that are currently being processed are in the processing set.
	// These things may be simultaneously in the dirty set. When we finish
	// processing something and remove it from this set, we'll check if
	// it's in the dirty set, and if so, add it to the queue.
	processing set

	cond *sync.Cond

	shuttingDown bool

	metrics queueMetrics

	unfinishedWorkUpdatePeriod time.Duration
	clock                      clock.Clock
}

// Get blocks until it can return an item to be processed. If shutdown = true,
// the caller should end their goroutine. You must call Done with item when you
// have finished processing it.
func (q *Type) Get() (item interface{}, shutdown bool) {
	q.cond.L.Lock()
	defer q.cond.L.Unlock()
	for len(q.queue) == 0 && !q.shuttingDown {
		q.cond.Wait()
	}
	if len(q.queue) == 0 {
		// We must be shutting down.
		return nil, true
	}

	item, q.queue = q.queue[0], q.queue[1:]

	q.metrics.get(item)

	q.processing.insert(item)
	q.dirty.delete(item)

	return item, false
}

Keyword synchronized realizes the waiting and notification mode

The keyword synchronized is combined with wait() and notify()/notifyAll() methods to realize the waiting and notification mode. When notifying with notify()/notifyAll() methods, the notified thread is randomly selected by the JVM. ReentrantLock combined with Condition class can realize the "selective notification" described earlier.

synchronized means that there is only one single Condition object in the whole Lock object, and all threads are registered on one object. When the thread starts notifyAll(), it needs to notify all WAITING threads. There is no choice, which will cause considerable efficiency problems.

See the following notes on Quartz source code for specific usage

Quartz uses synchronized to implement the waiting and notification mode

Next, let's learn how to use synchronized to realize the waiting and notification mode in the source code of the scheduling framework Quartz-2.3.0

       private final Object nextRunnableLock = new Object();

    /**
     * <p>
     * Run the given <code>Runnable</code> object in the next available
     * <code>Thread</code>. If while waiting the thread pool is asked to
     * shut down, the Runnable is executed immediately within a new additional
     * thread.
     * </p>
     *
     * @param runnable the <code>Runnable</code> to be added.
     */
    public boolean runInThread(Runnable runnable) {
        if (runnable == null) {
            return false;
        }

        synchronized (nextRunnableLock) {

            handoffPending = true;

            // Wait until a worker thread is available
            while ((availWorkers.size() < 1) && !isShutdown) {
                try {
                
                    // wait stops the thread
                    // Before calling wait(), the thread must get the object level lock of the object, that is, the wait method can only be invoked in the synchronization method or the synchronization block.

                    // The wait() method allows the thread calling the method to release the lock of the shared resource, then exit from the running state and enter the waiting queue until it is awakened again
                    nextRunnableLock.wait(500);
                } catch (InterruptedException ignore) {
                }
            }

            if (!isShutdown) {
                WorkerThread wt = (WorkerThread) availWorkers.removeFirst();
                busyWorkers.add(wt);
                wt.run(runnable);
            } else {
                // If the thread pool is going down, execute the Runnable
                // within a new additional worker thread (no thread from the pool).
                WorkerThread wt = new WorkerThread(this, threadGroup,
                        "WorkerThread-LastJob", prio, isMakeThreadsDaemons(), runnable);
                busyWorkers.add(wt);
                workers.add(wt);
                wt.start();
            }
            // notify keeps the stopped thread running
            // notify causes the thread in the wait state to exit the wait state and become waiting for the lock on the object (once the object is unlocked, they go back to compete)
            //  After notifyAll() is executed, the thread in wait state cannot obtain the object lock immediately. Wait until the thread executing notify() method finishes executing the program, that is, exit
            // After the synchronized code block, the current thread will release the lock, and all threads in the wait state can obtain the object lock

            // If the notify statement is not used, other threads waiting in the wait state need to continue blocking in the wait state because they have not been notified by the object until the object sends the notify()/notifyAll() notification

            // Before calling notifyAll(), the thread must get the object level lock of the object, that is, the notifyAll() method can only be invoked in the synchronization method or synchronization block.

            // notify() then wakes up the "one" thread waiting for the same shared resource in the waiting queue, and only notifies the "one" thread
            // notifyAll() wakes up "all" threads waiting for the same shared resource in the waiting queue, and only notifies "one" thread (the thread with the highest priority executes first or may execute immediately, depending on the implementation of the JVM virtual machine)
            nextRunnableLock.notifyAll();
            handoffPending = false;
        }

        return true;
    }

Topics: lock synchronized