How to realize inter thread communication in Java

Keywords: Java

The world kisses me with pain and asks me to return it with a song -- Tagore's collection of birds

Although usually each sub thread only needs to complete its own task, sometimes we want multiple threads to work together to complete a task, which involves inter thread communication.

With regard to inter thread communication, the methods and classes involved in this paper include: thread.join(), object.wait(), object.notify(), CountdownLatch, CyclicBarrier, FutureTask and Callable.

Next, we will introduce how to implement inter thread communication in Java with several examples:

  1. How to make two threads execute in turn, that is, one thread waits for the other thread to execute after it is completed?
  2. How to make two threads execute in an orderly way?
  3. There are four threads: A, B, C and D. how can d execute after a, B and C are executed synchronously?
  4. The three athletes prepare separately, and then start running at the same time when everyone is ready.
  5. After the child thread completes the task, it returns the result to the main thread.

1. How to make two threads execute in turn?

Suppose there are two threads: A and B. both threads can print numbers in order. The code is as follows:

public class Test01 {

    public static void main(String[] args) throws InterruptedException {
        demo1();
    }

    public static void demo1() {
        Thread a = new Thread(() -> {
            printNumber("A");
        });

        Thread b = new Thread(() -> {
            printNumber("B");
        });

        a.start();
        b.start();
    }

    public static void printNumber(String threadName) {
        int i = 0;
        while (i++ < 3) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + " print: " + i);
        }
    }

}

Copy code

The results obtained are as follows:

A print: 1
B print: 1
B print: 2
A print: 2
A print: 3
B print: 3
 Copy code

You can see that A and B print numbers at the same time. If we want B to start executing after A is completed, we can use the thread.join() method. The code is as follows:

public static void demo2() {
    Thread a = new Thread(() -> {
        printNumber("A");
    });

    Thread b = new Thread(() -> {
        System.out.println("B wait for A implement");
        try {
            a.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        printNumber("B");
    });

    a.start();
    b.start();
}

Copy code

The results obtained are as follows:

B wait for A implement
A print: 1
A print: 2
A print: 3
B print: 1
B print: 2
B print: 3
 Copy code

We can see that the a.join() method causes B to wait for a to finish printing.

The thread.join() method is used to block the current thread and wait for the thread calling the join() method to execute the following code.

Check the source code of the join() method. The internal call is join(0), as follows:

public final void join() throws InterruptedException {
    join(0);
}
Copy code

View the source code of join(0) as follows:

// Note that synchronized locking is used here, and the lock object is the instance object of the thread
public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
	// Call join(0) to execute the following code
    if (millis == 0) {
        // The purpose of using the while loop here is to avoid false wake-up
        // If the current thread survives, call wait (0). 0 means to wait forever until notifyAll() or notify() methods are called
        // The notifyAll() method is called when the thread ends
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
Copy code

It can be seen from the source code that the join(long millis) method is implemented through the wait(long timeout) (method provided by Object). Before calling the wait method, the current thread must obtain the lock of the Object. Therefore, the join method uses synchronized locking, and the lock Object is the instance Object of the thread. The wait(0) method will make the current thread block and wait until another thread calls the notify() or notifyAll() method of this Object. When the thread calling the join method ends, the notifyAll() method will be called, so the join() method can implement a thread to wait for another thread calling the join() to finish before executing.

False wake-up: a thread is awakened without being notified, interrupted or timed out;

False wake-up may lead to code execution when the conditions are not tenable and destroy the constraint relationship protected by the lock;

Why use a while loop to avoid false wakeups:

It is very dangerous to use the wait method in the if block. Once the thread is awakened and locked, it will not judge the if condition and execute the code outside the if statement block. Therefore, it is recommended to use the while loop to judge the condition first and then wait. The loop will test the condition before and after waiting.

2. How to make two threads intersect orderly in a specified way?

If we want thread B to print 1, 2 and 3 immediately after thread A prints 1, and then thread A continues to print 2 and 3, we need finer grained locks to control the execution order.

Here, we can use object.wait() and object.notify() methods. The code is as follows:

public static void demo3() {
    Object lock = new Object();
    Thread A = new Thread(() -> {
        synchronized (lock) {
            System.out.println("A 1");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A 2");
            System.out.println("A 3");
        }
    });

    Thread B = new Thread(() -> {
        synchronized (lock) {
            System.out.println("B 1");
            System.out.println("B 2");
            System.out.println("B 3");
            lock.notify();
        }
    });

    A.start();
    B.start();
}
Copy code

The results obtained are as follows:

A 1
B 1
B 2
B 3
A 2
A 3
 Copy code

The execution process of the above code is as follows:

  1. First, we create an object lock shared by A and B: lock = new Object();
  2. When A gets the lock, first print 1, then call the lock.wait() method to enter the wait state, then hand over the control of the lock.
  3. B will not be executed until A calls the lock.wait() method to release control and B obtains the lock;
  4. B prints 1, 2, 3 after getting the lock, then calls the lock.notify() method to wake up the A waiting.
  5. A continues to print the remaining 2 and 3 after waking up.

For ease of understanding, I added the above code to the log. The code is as follows:

public static void demo3() {
    Object lock = new Object();
    Thread A = new Thread(() -> {
        System.out.println("INFO: A Waiting to acquire lock");
        synchronized (lock) {
            System.out.println("INFO: A Get lock");
            System.out.println("A 1");
            try {
                System.out.println("INFO: A get into waiting State, relinquishing control of the lock");
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("INFO: A cover B Wake up and continue");
            System.out.println("A 2");
            System.out.println("A 3");
        }
    });

    Thread B = new Thread(() -> {
        System.out.println("INFO: B Waiting to acquire lock");
        synchronized (lock) {
            System.out.println("INFO: B Get lock");
            System.out.println("B 1");
            System.out.println("B 2");
            System.out.println("B 3");
            System.out.println("INFO: B At the end of execution, call notify Method wake up A");
            lock.notify();
        }
    });

    A.start();
    B.start();
}
Copy code

The results obtained are as follows:

INFO: A Waiting to acquire lock
INFO: A Get lock
A 1
INFO: A get into waiting State, relinquishing control of the lock
INFO: B Waiting to acquire lock
INFO: B Get lock
B 1
B 2
B 3
INFO: B At the end of execution, call notify Method wake up A
INFO: A cover B Wake up and continue
A 2
A 3
 Copy code

3. Thread D executes after synchronous execution of A, B and C

thread.join() allows A thread to continue executing after waiting for another thread to finish running. However, if we add A, B and C to the D thread in turn, we will let A, B and C execute in turn, and we want them to run synchronously.

The goal we want to achieve is: A, B and C threads can start running at the same time, and notify d when they run independently; D will not start running until a, B and C are all running. So we use CountdownLatch to implement this type of communication. Its basic usage is:

  1. Create a counter and set an initial value, CountdownLatch countDownLatch = new CountDownLatch(3);
  2. Call countDownLatch.await() to enter the waiting state until the count value becomes 0;
  3. Call countDownLatch.countDown() on other threads. This method will reduce the count value by one;
  4. When the value of the counter becomes 0, the method in the countDownLatch.await() waiting thread will continue to execute the following code.

The implementation code is as follows:

public static void runDAfterABC() {
    int count = 3;
    CountDownLatch countDownLatch = new CountDownLatch(count);
    new Thread(() -> {
        System.out.println("INFO: D wait for A B C Run complete");
        try {
            countDownLatch.await();
            System.out.println("INFO: A B C Operation completed, D Start running");
            System.out.println("D is working");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    for (char threadName = 'A'; threadName <= 'C' ; threadName++) {
        final String name = String.valueOf(threadName);
        new Thread(() -> {
            System.out.println(name + " is working");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " finished");
            countDownLatch.countDown();
        }).start();
    }
}
Copy code

The results obtained are as follows:

INFO: D wait for A B C Run complete
A is working
B is working
C is working
C finished
B finished
A finished
INFO: A B C Operation completed, D Start running
D is working
 Copy code

In fact, CountDownLatch itself is A countdown counter. We set the initial count value to 3. When D runs, first call the countDownLatch.await() method to check whether the counter value is 0. If it is not 0, it will remain in the waiting state. After A, B and C run, use the countDownLatch.countDown() method to reduce the countdown counter by 1. The counter will be decremented to 0, and then the await () method will be notified to end the wait and D will continue execution.

Therefore, CountDownLatch is applicable when a thread needs to wait for multiple threads.

4. The three athletes prepare to run at the same time

This time, A, B and C threads need to be prepared separately. When the three threads are ready, they start running at the same time. How should we do this?

CountDownLatch can be used to count, but when counting is completed, only one await() method of one thread will get a response, so multiple threads cannot be triggered at the same time. In order to achieve the effect of threads waiting for each other, we can use the CyclicBarrier. Its basic usage is:

  1. First, create a public object CyclicBarrier and set the number of threads waiting at the same time. CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
  2. These threads start to prepare at the same time. After they are ready, they need to wait for others to prepare, so they call the cyclicBarrier.await() method to wait for others;
  3. When the specified threads that need to wait at the same time call the cyclicBarrier.await() method, it means that these threads are ready, and these threads will start to continue to execute at the same time.

Imagine that three runners need to start running at the same time, so they need to wait for others to be ready. The implementation code is as follows:

public static void runABCWhenAllReady() {
    int count = 3;
    CyclicBarrier cyclicBarrier = new CyclicBarrier(count);
    Random random = new Random();
    for (char threadName = 'A'; threadName <= 'C' ; threadName++) {
        final String name = String.valueOf(threadName);
        new Thread(() -> {
            int prepareTime = random.nextInt(10000);
            System.out.println(name + " Preparation time:" + prepareTime);
            try {
                Thread.sleep(prepareTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " Ready, waiting for others");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(name + " Start running");
        }).start();
    }
}
Copy code

The results are as follows:

A Preparation time: 1085
B Preparation time: 7729
C Preparation time: 8444
A Ready, waiting for others
B Ready, waiting for others
C Ready, waiting for others
C Start running
A Start running
B Start running
 Copy code

The function of CyclicBarrier is to wait for multiple threads to execute at the same time.

5. The child thread returns the result to the main thread

In actual development, we often need to create sub threads to do some time-consuming tasks, and then send the execution results back to the main thread. So how to implement it in Java?

Generally, when creating a Thread, we will pass the Runnable object to the Thread for execution. The source code of Runable is as follows:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Copy code

You can see that Runable is a functional interface. The run method in this interface does not return a value. If you want to return results, you can use another similar interface Callable.

Functional interface: an interface with only one method

The source code of Callable interface is as follows:

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}
Copy code

As you can see, the biggest difference between Callable is that it returns generics.

So the next question is, how to pass back the results of the sub Thread? Java has a class, FutureTask, which can work with Callable, but note that the method used by get to get the result blocks the main Thread. FutureTask is essentially a Runnable, so it can be directly passed to the Thread.

For example, we want the child thread to calculate the sum of 1 to 100 and return the result to the main thread. The code is as follows:

public static void getResultInWorker() {
    Callable<Integer> callable = () -> {
        System.out.println("Subtask start execution");
        Thread.sleep(1000);
        int result = 0;
        for (int i = 0; i <= 100; i++) {
            result += i;
        }
        System.out.println("Subtask execution is completed and results are returned");
        return result;
    };
    FutureTask<Integer> futureTask = new FutureTask<>(callable);
    new Thread(futureTask).start();

    try {
        System.out.println("Start execution futureTask.get()");
        Integer result = futureTask.get();
        System.out.println("Results of execution:" + result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

Copy code

The results obtained are as follows:

Start execution futureTask.get()
Subtask start execution
 Subtask execution is completed and results are returned
 Result of execution: 5050
 Copy code

It can be seen that when the main thread calls the futureTask.get() method, the main thread is blocked; Then Callable starts executing internally and returns the result of the operation; Then futureask. Get () gets the result and the main thread resumes running.

Here we can see that FutureTask and Callable can directly obtain the results of child threads in the main thread, but they will block the main thread. Of course, if you don't want to block the main thread, you can consider using ExecutorService to add FutureTask to the thread pool to manage execution.


Author: cherish birds
Link: https://juejin.cn/post/7004401589385609246
Source: Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.

Posted by firecat318 on Sat, 20 Nov 2021 08:09:09 -0800