Using conditional lock to control the synchronization of multiple threads (java implementation)

Posted by moonman89 on Fri, 04 Mar 2022 13:34:38 +0100

catalogue

Title:

answer:

Explanation:

Recently, I did a topic of multithreading synchronization. I used conditional locking to solve it. By doing this problem, we can have a basic understanding of the application of locks. This article will briefly explain it.

Ps: after finishing, I found that this is the original question on Li buckle. The title link is: https://leetcode-cn.com/problems/print-zero-even-odd/ , here is my submission record:

You can see a variety of solutions to this problem on Likou. Please explore by yourself. This article only explains the implementation of conditional lock.

 

Title:

Suppose there is such a class:

class ZeroEvenOdd {
  public ZeroEvenOdd(int n) { ... } / / constructor
  public void zero(printNumber) { ... } / / print out only 0
  public void even(printNumber) { ... } / / print only even numbers
  public void odd(printNumber) { ... } / / print only odd numbers
}
The same instance of the ZeroEvenOdd class will be passed to three different threads:

Thread A will call {zero(), which outputs only 0.
Thread B will call {even(), which outputs only an even number.
Thread C will call {odd(), which outputs only odd numbers.
Each thread has a printNumber method to output an integer. Please modify the given code to output the integer sequence 010203040506, Where the length of the sequence must be 2n.

 

Example 1:

Input: n = 2
Output: "0102"
Note: three threads execute asynchronously. One thread calls zero(), the other thread calls even(), and the last thread calls odd(). The correct output is "0102".
Example 2:

Input: n = 5
Output: "0102030405"

answer:

package com.demo;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntConsumer;

public class ZeroEvenOddImpl1 {

    private int n;

    private int curr = 0;

    private ReentrantLock lock;

    private Condition cA;

    private Condition cB;

    private Condition cC;

    private volatile  int control = 0;

    public ZeroEvenOddImpl1(int n) {
        lock = new ReentrantLock();
        cA = lock.newCondition();
        cB = lock.newCondition();
        cC = lock.newCondition();
        this.n = n;
    }

    public static void main(String[] args) {
        ZeroEvenOddImpl1 impl = new ZeroEvenOddImpl1(10);
        Thread tA = new Thread( ()-> {
            try {
                impl.zero(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tB = new Thread( ()-> {
            try {
                impl.even(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tC = new Thread( ()-> {
            try {
                impl.odd(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        tC.start();
        tB.start();
        tA.start();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber)  throws InterruptedException {
        while(control<2);
        lock.lock();
        try {
            while(true) {
                if(curr >=n) {
                    cB.signal();
                    cC.signal();
                    return;
                }
                printNumber.accept(0);
                cC.signal();
                cA.await();
                if(curr >=n) {
                    cB.signal();
                    cC.signal();
                    return;
                }
                printNumber.accept(0);
                cB.signal();

                cA.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }

    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        lock.lock();
        control++;
        try {
            while (true) {
                cC.await();
                if(curr >=n) {
                    cA.signal();
                    return;
                }
                printNumber.accept(++curr);
                cA.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        lock.lock();
        control++;
        try {
            while(true) {
                cB.await();
                if(curr >=n) {
                    cA.signal();
                    return;
                }
                printNumber.accept(++curr);
                cA.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        } finally {
            lock.unlock();
        }
    }
}

Explanation:

Here we will first explain the api used in the code. The following is how to create conditional locks. One ReentrantLock can create multiple conditional locks.

            ReentrantLock lock = new ReentrantLock();

            Condition cA = lock.newCondition();

This code uses two methods of Condition: await and signal. The purpose of await is to block the current thread and release the lock resources. The function of signal is to wake up the thread calling await, and the awakened thread continues to execute from the place where await is called. These two functions need to be called after getting the lock, otherwise, throw the IllegalMonitorStateException exception, that is to say, you need to execute lock. first. Lock() (develop good coding habits, please release the lock resource in finally: lock.unlock()).

Next, we will explain the idea of solving the problem. This problem is essentially controlling the execution order of multiple threads (i.e. synchronization). We ignore the details of several threads in the topic and the execution sequence between threads, and only care about the control method. The idea here is to assign A separate conditional lock to each thread. Assuming that the conditional lock allocated to thread B is cB, the synchronization of thread B needs to be controlled by executing cB first Wait () to block yourself and wait for other threads to tell you to execute the task. If it is the turn of thread B to execute after the execution of thread A, thread A will call cB after the execution of the task Signal() wakes up the B thread. Similarly, if it is the turn of the C thread to execute after the B thread completes its execution, then it is also necessary to call the C Signal() wakes up the C thread and notifies the C thread to execute the task. With the idea of solving problems, it's easy to solve problems again. No matter how many threads there are in the topic and what execution order is between them, we can solve the problem as long as we put this idea up.

Finally, let's explain two small details in the code. 1: Careful readers have been thinking about the role of the control variable. In fact, the control variable is to ensure that Thread A obtains the lock after threads B and C obtain the lock. Why do you want to do this? Suppose A situation: Thread A obtains the lock first, then Thread A will execute CC Signal(), and at this time, the C Thread has not obtained the lock, CC Signal () will not play any role, which will cause the C lock to never be notified, resulting in the birth and death lock problem between threads, and all tasks are stuck. If Thread A obtains locks in the second order, there will be A similar problem. Some people may want to achieve the effect of letting A finally obtain the lock by delaying startup or using the isAlive method of Thread class, but this can not fully guarantee the purpose. It is unsafe code. 2: If n is an even number, and then comment out the release lock code in the finally code block of the even method, it will be found that the program cannot exit after execution. This is because although Thread B has notified Thread A after executing the operation, its own Thread will notify after notification. At this time, if the lock is not released, it cannot be really notified. So we must form A good habit of releasing resources.

Finally, a simple implementation of the problem is provided, but the implementation should be multi-threaded and unsafe. The submission on Niuke also prompts timeout. Readers with ideas are welcome to comment and discuss.

package com.demo;

import java.util.Currency;
import java.util.function.IntConsumer;

public class ZeroEvenOddImpl2 {

    private int n;

    private volatile int exec = 0;

    private volatile int curr = 0;

    public ZeroEvenOddImpl2(int n) {
        this.n = n;
    }

    public static void main(String[] args) {
        ZeroEvenOddImpl2 impl = new ZeroEvenOddImpl2(51);
        Thread tA = new Thread( ()-> {
            try {
                impl.zero(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tB = new Thread( ()-> {
            try {
                impl.even(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread tC = new Thread( ()-> {
            try {
                impl.odd(i -> System.out.print(i));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        tC.start();
        tB.start();
        tA.start();
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        while(curr < n) {
            while(exec!=0 && curr < n);
            if(curr < n) {
                printNumber.accept(0);
                exec = 1;
            }

            while(exec!=0 && curr < n);
            if(curr < n) {
                printNumber.accept(0);
                exec = 2;
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        while(curr<n) {
            while(exec!=2 && curr<n);
            if(curr<n) {
                printNumber.accept(++curr);
                exec = 0;
            }
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        while(curr<n) {
            while(exec!=1 && curr<n);
            if(curr<n) {
                printNumber.accept(++curr);
                exec = 0;
            }
        }
    }
}

 

 

 

 

 

 

 

Topics: Java leetcode Interview Multithreading Concurrent Programming