Interviewer: How does Java implement inter-thread communication?

 
  
 
  
 
  
 
  
您好,我是路人,更多优质文章见个人博客:http://itsoku.com

Under normal circumstances, each child thread can end after completing its respective task. But sometimes, we want multiple threads to work together to complete a certain task, which involves inter-thread communication.

Knowledge points involved in this article:

  1. thread.join(),

  2. object.wait(),

  3. object.notify(),

  4. CountdownLatch,

  5. CyclicBarrier,

  6. FutureTask,

  7. Callable 。

This article involves code: https://github.com/wingjay/HelloJava/blob/master/multi-thread/src/ForArticle.java

Next, I will use a few examples as an entry point to explain what methods are available in Java to achieve inter-thread communication.

  1. How to make two threads execute sequentially?

  2. So how to make two threads run in an orderly manner in a specified way?

  3. Four threads ABCD, among which D will not be executed until ABC is fully executed, and ABC runs synchronously

  4. The three athletes prepare separately, and then run together when all three are ready

  5. After the child thread completes a certain task, it returns the result to the main thread


How to make two threads execute sequentially?


Suppose there are two threads, one is thread A and the other is thread B, and the two threads can print the 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();  
}

The printNumber(String) is implemented as follows, which is used to print three numbers 1, 2, and 3 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 point we get the result:

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 all printed? 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 开始等待 A");  
            try {  
                A.join();  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            printNumber("B");  
        }  
    });  
    B.start();  
    A.start();  
}

The results obtained are as follows:

B starts waiting for AA 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 make B wait until A finishes running.

So how to make two threads run in an orderly manner in a specified way?

Still the above example, now I want A to print 1, 2, 3 after printing 1, and finally return to A to continue printing 2, 3. Under this demand, it is obvious that Thread.join() can no longer be satisfied. We need finer-grained locks to control execution order.

Here, we can use object.wait () and object.notify () two methods to achieve. code show as below:

/**  
 * 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 printed results are as follows:

A 1 A waiting…

B 1 B 2 B 3 A 2 A 3

Exactly what we wanted.

So, what happened in this process?

  1. First 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, hand over the control of the lock, and enter the wait state;

  3. For B, since A got the lock at first, B cannot execute; B didn't get the lock until A called lock.wait() to release control;

  4. B prints 1, 2, 3 after getting the lock; then calls the lock.notify() method to wake up A that is waiting;

  5. After A wakes up, continue to print the remaining 2, 3.

For better understanding, I added 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 等待锁 ");  
            synchronized (lock) {  
                System.out.println("INFO: A 得到了锁 lock");  
                System.out.println("A 1");  
                try {  
                    System.out.println("INFO: A 准备进入等待状态,放弃锁 lock 的控制权 ");  
                    lock.wait();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                System.out.println("INFO: 有人唤醒了 A, A 重新获得锁 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 等待锁 ");  
            synchronized (lock) {  
                System.out.println("INFO: B 得到了锁 lock");  
                System.out.println("B 1");  
                System.out.println("B 2");  
                System.out.println("B 3");  
                System.out.println("INFO: B 打印完毕,调用 notify 方法 ");  
                lock.notify();  
            }  
        }  
    });  
    A.start();  
    B.start();  
}

The printed results are as follows:

INFO: A is waiting for the lock INFO: A has obtained the lock A 1 INFO: A is ready to enter the waiting state, call lock.wait() to give up the control of the lock INFO: B is waiting for the lock INFO: B has obtained the lock B 1 B 2 B 3 INFO: B finished printing, call the lock.notify() method INFO: Someone woke up A, A regained the lock A 2 A 3

Four threads ABCD, among which D will not be executed until ABC is fully executed, and ABC runs synchronously

At the beginning, we introduced thread.join(), which allows one thread to wait for another thread to finish running before continuing to execute. Then we can join ABC in D thread in sequence, but this also makes ABC must be executed in sequence, and we want The best part is that all three can run in sync.

In other words, what we hope to achieve is: ABC three threads run at the same time, and notify D after running independently; for D, as long as ABC is finished running, D will start running again. In response to this situation, we can use CountdownLatch to implement this type of communication. Its basic usage is:

  1. Create a counter, set the initial value, CountDownLatch countDownLatch = new CountDownLatch(2);

  2. Call the countDownLatch.await() method in the waiting thread to enter the waiting state until the count value becomes 0;

  3. In other threads, call the countDownLatch.countDown() method, which will decrease the count value by 1;

  4. When the countDown() method of other threads changes the count value to 0, the countDownLatch.await() in the waiting thread exits immediately and continues 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();  
    }  
}

The following is the result of running:

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, to put it simply, CountDownLatch is a countdown counter. We set the initial count value to 3. When D is running, first call countDownLatch.await() to check whether the counter value is 0. If it is not 0, it will keep waiting; when ABC will use countDownLatch.countDown() to decrement the countdown counter by 1 after each finishes running. When all three are finished running, the counter will be decremented to 0; at this time, D's await() will be triggered immediately and the execution will continue downward.

Therefore, CountDownLatch is suitable for situations where one thread waits for multiple threads.

The three athletes prepare separately, and then run together when all three are ready

The above is a vivid metaphor, starting preparations for threads ABC, until all three are ready, and then run at the same time. That is, to achieve an effect of waiting for each other between threads, how should it be achieved?

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

In order to realize the need for threads to wait for each other, we can use the CyclicBarrier data structure. Its basic usage is:

  1. First create a public CyclicBarrier object, 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 their preparations are completed, they need to wait for others to complete their preparations. At this time, call cyclicBarrier.await(); to start waiting for others;

  3. When the specified number of threads waiting at the same time call cyclicBarrier.await();, it means that these threads are all ready, and then these threads continue to execute at the same time.

The implementation code is as follows, imagine that there are three runners, each of them waits for the others when they are ready, and starts running after 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(); // 当前运动员准备完毕,等待别人准备好  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                } catch (BrokenBarrierException e) {  
                    e.printStackTrace();  
                }  
                System.out.println(rN + " starts running"); // 所有运动员都准备好了,一起开始跑  
            }  
        }).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 child thread completes a certain 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 to implement this situation in Java?

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

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

You can see that run() will not return any results after execution. What if you want to return the result? Another similar interface class Callable can be used here:

@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 is to return the result of generic type V.

Then the next question is, how to return the result of the sub-thread? In Java, there is a class that is used with Callable: FutureTask, but note that its get method for obtaining results will block 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 printed results are as follows:

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

It can be seen that the main thread blocks the main thread when calling the futureTask.get() method; then the Callable starts to execute internally and returns the operation result; at this time, futureTask.get() gets the result, and the main thread resumes running.

Here we can learn that through FutureTask and Callable, the operation results of the child threads can be obtained directly 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 in 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. This article gives a general explanation of Java inter-thread communication, and will explain thread synchronization and thread safety later.

1556c604e458edb3a230f362243570bb.png

↓  Click to read the original text and go directly to my personal blog

59ba5c6bf1f1cf9b88c4be6e6b6a8b79.jpeg Are you looking

Guess you like

Origin blog.csdn.net/likun557/article/details/131606545