5
头图
The world kissed me bitterly, and asked me to repay it with a song—— Tagore's "Asuka"

Although usually each child 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.

The methods and classes involved in inter-thread communication include: thread.join() , object.wait() , object.notify() , CountdownLatch , CyclicBarrier , FutureTask , Callable .

Next, a few examples will be used to introduce how to implement inter-thread communication in Java:

  1. How to make two threads execute sequentially, that is, one thread waits for the other thread to complete before executing?
  2. How to make two threads intersect and execute in a specified way in an orderly manner?
  3. has four threads: A, B, C, and D. How to realize that D is executed after A, B, and C are all executed synchronously?
  4. three athletes prepare separately, and then start running at the same time after everyone is ready.
  5. child thread completes the task, it returns the result to the main thread.

1. How to make two threads execute sequentially?

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

}

The results obtained are as follows:

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

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

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

    Thread b = new Thread(() -> {
        System.out.println("B 等待 A 执行");
        try {
            a.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        printNumber("B");
    });

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

The results obtained are as follows:

B 等待 A 执行
A print: 1
A print: 2
A print: 3
B print: 1
B print: 2
B print: 3

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

thread.join() method is to block the current thread and wait for the thread that called the join() method to execute before executing the following code.

Check join() method, the internal call is join(0) , as follows:

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

View the source code of join(0)

// 注意这里使用了 sychronized 加锁,锁对象是线程的实例对象
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");
    }
    // 调用 join(0) 执行下面的代码
    if (millis == 0) {
        // 这里使用 while 循环的目的是为了避免虚假唤醒
        // 如果当前线程存活则调用 wait(0), 0 表示永久等待,直到调用 notifyAll() 或者 notify() 方法
        // 当线程结束的时候会调用 notifyAll() 方法
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

As can be seen from source join(long millis) method by wait(long timeout) ( Object implemented method for providing a) a method call wait before the method, the current thread must acquire the lock object, so this join method uses synchronized lock, the lock object is an instance of the thread Object. Which wait(0) method will make the current thread blocks until another thread calls this object of notify() or notifyAll() method will continue. When the thread calling the join method ends, the notifyAll() method will be called, so the join() method can implement a thread waiting for another thread calling join() to end before executing.

false wakeup : a thread is awakened without being notified, interrupted, or timed out;

False wakeup may cause code to be executed when the condition is not established, destroying the constraint relationship of the lock protection;

Why use a while loop to avoid falsely waking up :

It is very dangerous to use the wait method in the if block, because once the thread is awakened and locked, it will not judge the if condition and execute the code outside the if statement block, so it is recommended to do conditional judgment first before waiting Wherever, the while loop is used to do it, the loop will test the conditions before and after waiting.

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

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

Here, we can use the 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();
}

The results obtained are as follows:

A 1
B 1
B 2
B 3
A 2
A 3

The execution flow 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, it prints 1 first, then calls the lock.wait() method to enter the waiting state, and then hands 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. After B gets the lock, it prints 1, 2, 3, and then calls the lock.notify() method to wake up the waiting A;
  5. A Continue to print the remaining 2, 3 after waking up.

In order to facilitate 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 等待获取锁");
        synchronized (lock) {
            System.out.println("INFO:A 获取到锁");
            System.out.println("A 1");
            try {
                System.out.println("INFO:A 进入 waiting 状态,放弃锁的控制权");
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("INFO:A 被 B 唤醒继续执行");
            System.out.println("A 2");
            System.out.println("A 3");
        }
    });

    Thread B = new Thread(() -> {
        System.out.println("INFO:B 等待获取锁");
        synchronized (lock) {
            System.out.println("INFO:B 获取到锁");
            System.out.println("B 1");
            System.out.println("B 2");
            System.out.println("B 3");
            System.out.println("INFO:B 执行结束,调用 notify 方法唤醒 A");
            lock.notify();
        }
    });

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

The results obtained are as follows:

INFO:A 等待获取锁
INFO:A 获取到锁
A 1
INFO:A 进入 waiting 状态,放弃锁的控制权
INFO:B 等待获取锁
INFO:B 获取到锁
B 1
B 2
B 3
INFO:B 执行结束,调用 notify 方法唤醒 A
INFO:A 被 B 唤醒继续执行
A 2
A 3

3. Thread D is executed after A, B, and C are all executed synchronously

thread.join() The method described earlier allows one thread to continue execution after waiting for another thread to finish running. But if we add A, B, and C to the D thread in turn, A, B, and C will be executed in turn, and we want them to run synchronously.

The goal we want to achieve is: A, B, and C three threads can start running at the same time, and notify D after each independent operation is completed; D will not start running until A, B, and C are all finished. So we use CountdownLatch to achieve 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() enter the waiting state until the count value becomes 0;
  3. countDownLatch.countDown() in other threads, this method will decrement the count value by one;
  4. When the value of the counter becomes 0 , 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 等待 A B C 运行完成");
        try {
            countDownLatch.await();
            System.out.println("INFO: A B C 运行完成,D 开始运行");
            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();
    }
}

The results obtained are as follows:

INFO: D 等待 A B C 运行完成
A is working
B is working
C is working
C finished
B finished
A finished
INFO: A B C 运行完成,D 开始运行
D is working

In fact, CountDownLatch itself is a countdown counter, we set the initial count value to 3. When D is running, first call the countDownLatch.await() method to check whether the value of the counter is 0, if it is not 0, keep the waiting state. After A, B, and C have finished running, use the countDownLatch.countDown() method to decrement the countdown counter by 1. The counter will decrease to 0, and then notify await() method ends and wait, and D starts to continue execution.

Therefore, CountDownLatch suitable for situations where one thread needs to wait for multiple threads.

4. Three athletes prepare to start running at the same time separately

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

CountDownLatch can be used to count, but when the count 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 call the cyclicBarrier.await() method to wait for others;
  3. When the specified threads that need to wait at the same time all call the cyclicBarrier.await() method, it means that these threads are ready, then these threads will start to continue executing at the same time.

Imagine that there are three runners who need to start running at the same time, so they need to wait for everyone else 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 + " 准备时间:" + prepareTime);
            try {
                Thread.sleep(prepareTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " 准备好了,等待其他人");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(name + " 开始跑步");
        }).start();
    }
}

The results are as follows:

A 准备时间:1085
B 准备时间:7729
C 准备时间:8444
A 准备好了,等待其他人
B 准备好了,等待其他人
C 准备好了,等待其他人
C 开始跑步
A 开始跑步
B 开始跑步

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 child threads to do some time-consuming tasks, and then pass 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 Thread execution. The source code of Runable

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

You can see that Runable is a functional interface. The run method in this interface has no return value. If you want to return the result, you can use another similar interface Callable .

Functional interface: an interface with only one method

The source code of the 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 Callable is that it returns a generic type.

So the next question is, how to pass the result of the child thread back? Java has a class, FutureTask , which can work with Callable , but please note that get used to get the result will block the main thread. FutureTask is essentially a Runnable , so it can be passed directly to Thread .

image-20210905183800030.png

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("子任务开始执行");
        Thread.sleep(1000);
        int result = 0;
        for (int i = 0; i <= 100; i++) {
            result += i;
        }
        System.out.println("子任务执行完成并返回结果");
        return result;
    };
    FutureTask<Integer> futureTask = new FutureTask<>(callable);
    new Thread(futureTask).start();

    try {
        System.out.println("开始执行 futureTask.get()");
        Integer result = futureTask.get();
        System.out.println("执行的结果:" + result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

The results obtained are as follows:

开始执行 futureTask.get()
子任务开始执行
子任务执行完成并返回结果
执行的结果:5050

It can be seen that when the main thread calls the futureTask.get() method, the main thread is blocked; then Callable starts to execute internally and returns the result of the operation; then futureTask.get() gets the result, and the main thread resumes operation.

Here we can understand that FutureTask and Callable can directly get the results of the 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 transfer FutureTask to the thread pool to manage execution.

Reference article:

https://www.tutorialdocs.com/article/java-inter-thread-communication.html


惜鸟
328 声望2.3k 粉丝