正常情况下,每个子线程完成各自的任务就可以结束了,但是有时候,需要多个线程协同工作来完成任务,这个时候就涉及到线程间的通讯了。

本文涉及知识点:

  1. thread.join(),
  2. object.wait(),
  3. object.notify(),
  4. CountdownLatch,
  5. CyclicBarrier,
  6. FutureTask,
  7. Callable 。

下面将用几个例子作为切入点来讲解Java里有那些方法来实现线程通信。

1,如何让两个线程依次执行

2,如何让两个线程按照指定的方式交叉运行

3,四个线程A,B,C,D,其中D要等待A B C全部执行后才能执行,并且A B C是同步运行的

4,三个运动员各自准备,等到3个人都准备好之后,在一起比赛

5,子线程完成某个任务后,把得到的结果回传给主线程。

如何让两个线程依次执行

假设有两个线程,一个是线程A,一个是线程B,两个线程分别依次打印1-3三个数字,相关代码如下

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)实现如下,用来依次打印1,2,3这三个数字:

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

这时候我们得到的结果是:

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

可以看到A和B是同时打印的。

如果我们想要在B在A全部打印完成之后再开始打印呢,我们可以用thread.join()方法,代码如下:

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

得到的结果如下:

B 开始等待 A
A print: 1
A print: 2
A print: 3

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

所以可以看到A.join()方法会让B一直等待,直到A运行完毕。

如何让两个线程按照指定的方式有序交叉运行

还是上面的那个例子,我们希望A在打印完1之后,让B打印1,2,3,最后再回到A打印2,3,这种需求下,显然thread.join()已然不适合,我们需要更加细粒度的锁来控制执行顺序。

我们可以用object.wait()和object.notify()两个方法来实现,代码如下:

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

打印结果如下:

A 1
A waiting…

B 1
B 2
B 3
A 2
A 3

这正是我们需要的结果,那么在这个过程中发生了什么呢?

1,首先,创建一个A和B共享的对象所lock = new Object();

2,当A得到锁之后,先打印1,然后调用了lock.wait()方法,交出了锁的控制权,进入wait状态

3,对于B而言,由于A最开始获取了锁,导致B无法运行,直到A调用了lock.wait()之后释放了锁的控制权,B才得到了锁

4,B在得到锁之后打印1,2,3,然后调用lock.notify()方法,欢迎正在wait的A

5,A被唤醒后,继续打印剩下的2,3

四个线程A,B,C,D,其中D要等到A B C全部执行完毕后才执行,而且A B C是同步运行的

最开始我们介绍了thread.join(),可以让一个线程等待另外一个线程执行完毕之后在执行,那我们可以在D线程里依次join A B C,不过这样的话,A B C就要依次执行了,并不是同步运行的

或者说我们希望:A B C三个线程同步运行,各自独立完成任务后通知D,对于D而言,只要A B C都运行完了,D才能开始运行,针对这种情况,我们可以用CountDownLatch来实现这种通信方式,他的基本用法:

1,创建一个计算器,设置初始值,CountDownLatch cdl = new CountDownLatch(3);

2,在线程里调用cdl.await()方法,进入等到状态,直到计算值变成0

3,在其他线程里,调用cdl.countDown()方法,该方法会将计数值减1

4,当其他线程countDown()方法把计算值变成0时,等待线程里的countDownLatch.await()立即退出,开始执行下面的代码。

实现代码如下:

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

下面是运行结果:

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

A finished
C finished
B finished

其实,简单点来说,CountDownLatch就是一个倒数计数器,我们把初始数值设置为3,当D运行时明会先调用countDownLatch.await()检查计数器值是否是0,如果不为0则继续保持等待状态,当A B C各自都完成后都会调用countDown()方法将倒数计数器减1,当三个都运行后,计数器被减成0,此时立即出发D的await(),运行结束,继续向下推进。

三个运动员各自准备,等待三个人都准备好之后,在一起跑

上面是一个形象的比喻,针对线程A B C各自开始准备,直到三者都准备完毕,然后在同时运行,也就是要实现一种线程之间互相等待的结果,那应该如何进行实现?

上面的countDownLatch可以用来倒计数,但当计数完毕,只有一个线程await()会得到相应,无法让多个线程同时触发。

为了实现这种需求,可以使用CyclicBarrier数据结构,他的基本用法:

1,先创建一个公共的CyclicBarrier,设置同时等待的线程数,CyclicBarrier cb = new CyclicBarrier(3);

2,这些线程同时开始自己准备,自身准备完毕之后,需要等待别人准备完毕,这时调用cb.await(),即可开始等待别人

3,当指定的“同时等待的线程数”都调用了await方法之后,就意味着这些线程全都准备完毕,然后这些线程才开始同时执行

实现代码如下:

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

打印结果如下:

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
 

子线程完成后,把得到的结果传递给主线程

实际开发过程中,我们经常要创建一些子线程来执行一些比较耗时的任务,然后把任务的执行结果返回给主线程,这种情况下如何实现?

一般创建线程,都是把Runnable对象传递给Thread对象去执行,Runnable定义如下:

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

我们看到run方法在执行完毕之后不会有任何的返回结果,如果我们想要得到返回结果的话,可以使用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;  
}

可以看出最大的区别在于Callable有一个返回结果(泛型V)

在java中有一个类是配合Callable使用的:FuntureTask,不过注意,他获取结果的时候会阻塞主线程

例如:我们想要让子线程执行计算1-100的和,并把返回的结果返回给主线程

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

打印结果如下:

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

可以看到,主线程调用funtureTask.get()会阻塞主线程,然后在Callable内部执行,并返回运算结果,此时funtureTask.get()得到结果,主线程恢复运行。

如果不希望阻塞主线程,可以考虑ExecutorService,把FuntureTask放到线程池去管理执行即可。