How does Java implement inter thread communication?

Keywords: Java

Under normal circumstances, each sub thread can finish its own task. However, sometimes we want multiple threads to work together to complete a task, which involves inter thread communication.

Knowledge points involved in this paper:

thread.join(),
object.wait(),
object.notify(),
CountdownLatch,
CyclicBarrier,
FutureTask,
Callable .

Let me use a few examples as a starting point to explain what methods there are in Java to realize inter thread communication.

How to make two threads execute in turn?
So how to make two threads run in an orderly way?
There are four threads a, B, C and D, in which D will not be executed until all a, B and C are executed, and a, B and c run synchronously
The three athletes prepare themselves and wait until all three are ready before running together
After the sub thread completes a task, it returns the result to the main thread
How to make two threads execute in turn?
Suppose there are two threads, one is thread A and the other is thread B. the two threads can print three numbers 1-3 in turn. Let's look at the code:

private static void demo1() {  
    Thread A = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            printNumber("A");  
        }  
    });  
    Thread B = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            printNumber("B");  
        }  
    });  
    A.start();  
    B.start();  
}  

printNumber(String) is implemented as follows to print 1, 2 and 3 numbers in sequence:

private 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);  
    }  
}  

At this time, we get the following results:

B print: 1 A print: 1 B print: 2 A print: 2 B print: 3 A print: 3

You can see that A and B are printed at the same time.

So, what if we want B to start printing after A has finished printing? We can use the thread.join() method. The code is as follows:

private static void demo2() {  
    Thread A = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            printNumber("A");  
        }  
    });  
    Thread B = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            System.out.println("B Start waiting A");  
            try {  
                A.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            printNumber("B");  
        }  
    });  
    B.start();  
    A.start();  
}  

The results obtained are as follows:

B Start waiting A A print: 1 A print: 2 A print: 3

B print: 1 B print: 2 B print: 3

So we can see that the A.join() method will keep B waiting until a runs.

So how to make two threads run in an orderly way?
As in the above example, I now want A to print 1, 2 and 3 after printing 1, and finally return to A to continue printing 2 and 3. Obviously, Thread.join() can no longer meet this requirement. We need finer grained locks to control the order of execution.

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

/**  
 * A 1, B 1, B 2, B 3, A 2, A 3  
 */  
private static void demo3() {  
    Object lock = new Object();  
    Thread A = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            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(new Runnable() {  
        @Override  
        public void run() {  
            synchronized (lock) {  
                System.out.println("B 1");  
                System.out.println("B 2");  
                System.out.println("B 3");  
                lock.notify();  
            }  
        }  
    });  
    A.start();  
    B.start();  
}  

The printing results are as follows:

A 1 A waiting...

B 1 B 2 B 3 A 2 A 3

That's what we want.

So what happened to this process?

First, create an object lock shared by A and B, lock = new Object();
When A gets the lock, first print 1, then call the lock.wait() method to hand over the control of the lock and enter the wait state.
For B, because A got the lock at the beginning, B could not execute it; B won't get the lock until A calls lock.wait() to release control;
B print 1, 2 and 3 after obtaining the lock; Then call the lock.notify() method to wake up the A that is in wait;
After A is awakened, continue to print the remaining 2 and 3.
In order to better understand, I add log to the above code for readers to view.

private static void demo3() {  
    Object lock = new Object();  
    Thread A = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            System.out.println("INFO: A Waiting lock ");  
            synchronized (lock) {  
                System.out.println("INFO: A Got the lock lock");  
                System.out.println("A 1");  
                try {  
                    System.out.println("INFO: A Ready to enter the waiting state and give up the lock lock Control of ");  
                    lock.wait();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                System.out.println("INFO: Someone woke up A, A Regain lock lock");  
                System.out.println("A 2");  
                System.out.println("A 3");  
            }  
        }  
    });  
    Thread B = new Thread(new Runnable() {  
        @Override  
        public void run() {  
            System.out.println("INFO: B Waiting lock ");  
            synchronized (lock) {  
                System.out.println("INFO: B Got the lock lock");  
                System.out.println("B 1");  
                System.out.println("B 2");  
                System.out.println("B 3");  
                System.out.println("INFO: B After printing, call notify method ");  
                lock.notify();  
            }  
        }  
    });  
    A.start();  
    B.start();  
}  

The printing results are as follows:

INFO: A Waiting lock INFO: A Got the lock lock A 1 INFO: A Ready to enter the waiting state, call lock.wait() Discard lock lock Control of INFO: B Waiting lock INFO: B Got the lock lock B 1 B 2 B 3 INFO: B After printing, call lock.notify() method INFO: Someone woke up A, A Regain lock lock A 2 A 3

There are four threads a, B, C and D, in which D will not be executed until all a, B and C are executed, and a, B and c run synchronously
At the beginning, we introduced thread.join(), which allows one thread to wait for another thread to run before continuing. Then we can join a, B and C in D thread in turn, but this makes a, B and C must execute in turn, and what we want is that the three can run synchronously.

In other words, we hope to achieve the following goals: A, B and C threads run at the same time, and notify d after each thread runs independently; For D, as long as a, B and C are running, D will start running again. In view of this situation, we can use CountdownLatch to realize this kind of communication mode. Its basic usage is:

1. Create a counter and set the initial value, CountdownLatch countDownLatch = new CountDownLatch(2);
2. Call countDownLatch.await() method in the waiting thread to enter the waiting state until the count value becomes 0;
3. In other threads, call countDownLatch.countDown() method, which will reduce the count value by 1;
4. When the countDown() method of other threads changes the count value to 0, wait for countDownLatch.await() in the thread to exit immediately and continue to execute the following code.

The implementation code is as follows:

private static void runDAfterABC() {  
    int worker = 3;  
    CountDownLatch countDownLatch = new CountDownLatch(worker);  
    new Thread(new Runnable() {  
        @Override  
        public void run() {  
            System.out.println("D is waiting for other three threads");  
            try {  
                countDownLatch.await();  
                System.out.println("All done, D starts working");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }).start();  
    for (char threadName='A'; threadName <= 'C'; threadName++) {  
        final String tN = String.valueOf(threadName);  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                System.out.println(tN + " is working");  
                try {  
                    Thread.sleep(100);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                System.out.println(tN + " finished");  
                countDownLatch.countDown();  
            }  
        }).start();  
    }  
}  

Here are the results:

D is waiting for other three threads A is working B is working C is working

A finished C finished B finished All done, D starts working

In fact, simply put, CountDownLatch is an inverted counter. We set the initial count value to 3. When D runs, first call countDownLatch.await() to check whether the counter value is 0. If it is not 0, keep waiting; When a, B and c run respectively, they will use countDownLatch.countDown() to reduce the countdown counter by 1. When all three run, the counter will be reduced to 0; At this point, immediately trigger the await() operation of D to end and continue to execute downward.

Therefore, CountDownLatch is suitable for a thread waiting for multiple threads.

The three athletes prepare themselves and wait until all three are ready before running together
The above is a vivid metaphor. For threads a, B and C, start to prepare respectively until all three are ready, and then run at the same time. That is, to achieve the effect of waiting for each other between threads, how should we achieve it?

The CountDownLatch above can be used to count down, but when the count is completed, only one thread's await() will get a response, and multiple threads cannot be triggered at the same time.

In order to meet the requirement of waiting for each other between threads, we can use the CyclicBarrier data structure. Its basic usage is:

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

The implementation code is as follows. Imagine that there are three runners who wait for others when they are ready and start running when they are all ready:

private static void runABCWhenAllReady() {  
    int runner = 3;  
    CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);  
    final Random random = new Random();  
    for (char runnerName='A'; runnerName <= 'C'; runnerName++) {  
        final String rN = String.valueOf(runnerName);  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                long prepareTime = random.nextInt(10000) + 100;  
                System.out.println(rN + " is preparing for time: " + prepareTime);  
                try {  
                    Thread.sleep(prepareTime);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                try {  
                    System.out.println(rN + " is prepared, waiting for others");  
                    cyclicBarrier.await(); // The current athlete is ready, waiting for others to be ready  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                } catch (BrokenBarrierException e) {  
                    e.printStackTrace();  
                }  
                System.out.println(rN + " starts running"); // All the athletes are ready to start running together  
            }  
        }).start();  
    }  
}  

The printed results are as follows:

A is preparing for time: 4131 B is preparing for time: 6349 C is preparing for time: 8206 A is prepared, waiting for others B is prepared, waiting for others C is prepared, waiting for others C starts running A starts running B starts running

After the sub thread completes a task, it 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 task execution results back to the main thread for use. How can we achieve this in Java?

Reviewing the creation of threads, we generally pass the runnable object to the Thread for execution. Runnable is defined as follows:

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

You can see that run() does not return any results after execution. What if you want to return the result? Here, you can use another similar interface class Callable:

@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;  
}  

It can be seen that the biggest difference between Callable and Callable is that it returns the normal V result.

So the next question is, how to send back the results of the sub thread? In Java, there is a class that works with Callable: FutureTask, but note that the get method it gets the result blocks the main thread.

For example, we want the child thread to calculate from 1 to 100 and return the calculated result to the main thread.

private static void doTaskWithResultInWorker() {  
    Callable<Integer> callable = new Callable<Integer>() {  
        @Override  
        public Integer call() throws Exception {  
            System.out.println("Task starts");  
            Thread.sleep(1000);  
            int result = 0;  
            for (int i=0; i<=100; i++) {  
                result += i;  
            }  
            System.out.println("Task finished and return result");  
            return result;  
        }  
    };  
    FutureTask<Integer> futureTask = new FutureTask<>(callable);  
    new Thread(futureTask).start();  
    try {  
        System.out.println("Before futureTask.get()");  
        System.out.println("Result: " + futureTask.get());  
        System.out.println("After futureTask.get()");  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    } catch (ExecutionException e) {  
        e.printStackTrace();  
    }  
}  

The printing results are as follows:

Before futureTask.get() Task starts Task finished and return result Result: 5050 After futureTask.get()

You can see that the main thread blocks the main thread when calling the futureTask.get() method; Then Callable starts execution internally and returns the operation result; At this point, futureask. Get () gets the result and the main thread resumes running.

Here we can learn that FutureTask and Callable can directly obtain the operation results of sub threads in the main thread, but the main thread needs to be blocked. Of course, if you don't want to block the main thread, you can consider using ExecutorService to put FutureTask into the thread pool to manage execution.

Summary
Multithreading is a common feature of modern languages, and inter thread communication, thread synchronization and thread safety are very important topics.

Posted by piet on Tue, 21 Sep 2021 00:56:31 -0700