Java 如何实现线程间通信

正常情况下,每个子线程完成各自的任务就可以结束了。不过有的时候,我们希望多个线程协同工作来完成某个任务,这个时候就涉及到了线程间的通信

涉及到的内容主要有:

  1. thread.join()
  2. object.wait()
  3. objet.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. 三个运动员各自准备,等到三个人都准备好后,再一起跑
  5. 子线程完成某件任务后,把得到的结果回传给主线程

如何让两个线程依次执行?

假设有两个线程,一个线程 A, 另外一个线程 B,两个线程分别依次打印1-3三个数字即可,代码实现如下:

public class ThreadDemo1
{

    /**
     * 
     */
    public ThreadDemo1()
    {
        // TODO Auto-generated constructor stub
    }
    
    public static void demo1() {
        Thread A =new Thread(new Runnable() {
            public void run() {
                printNumber("A");
            }
        });
        Thread B=new Thread(new Runnable() {
            public void run() {
                printNumber("B");
            }
        });
        A.start();
        B.start();
    }

    protected static void printNumber( String threadName )
    {
        // TODO Auto-generated method stub
        int i=0;
        while(i++<3) {
            try {
                Thread.sleep(100);
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName+" print: "+i);
        }
        
    
        
    }

    /**
     * @param args
     */
    public static void main( String[] args )
    {
        // TODO Auto-generated method stub
       demo1();
    }

}

运行结果如下:

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

从运行结果可以看到线程 A和线程 B是同时打印的。

那么,如果我们希望 B在A全部打印完成后再开始打印呢?我们可以利用thread.join()方法实现,代码如下:

package com.test.thread;

public class ThreadJoinDemo {

    public ThreadJoinDemo() {
        // TODO Auto-generated constructor stub
    }

    public static void demo1() {
        Thread A = new Thread(new Runnable() {
            public void run() {
                printNumber("A");
            }
        });
        Thread B = new Thread(new Runnable() {
            public void run() {
                System.out.println("B 开始等待  A");
                try {
                    A.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                printNumber("B");
            }
        });
        A.start();
        B.start();
    }

    protected static void printNumber(String threadName) {
        // TODO Auto-generated method stub
        int i = 0;
        while (i++ < 3) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + " print: " + i);
        }

    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        demo1();
    }

}

运行结果如下:

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()两个方法来实现,代码实现如下:

package com.test.thread;

public class ThreadDemo3 {

    public ThreadDemo3() {
        // TODO Auto-generated constructor stub
    }

    
    private static void demo3() {
        Object lock=new Object();
        Thread A=new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                synchronized(lock) {
                   System.out.println("A1");
                   try {
                   
                       lock.wait();
                 
                   }catch(InterruptedException e) {
                      e.printStackTrace();
                   }
                }
                System.out.println("A2");
                System.out.println("A3");
       
            }
       
        });
        Thread B=new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                synchronized(lock) {
                    System.out.println("B1");
                    System.out.println("B2");
                    System.out.println("B3");
                    lock.notify();
                }
            }
            
        });
        A.start();
        B.start();
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        demo3();
    }

}

运行结果如下:

A1
B1
B2
B3
A2
A3

从运行结果看正是我们想要的结果,那么这个过程发生了什么,先分析如下:

  1. 首先创建一个A和B共享的对象锁lock=new Object();
  2. 当A得到锁后,先打印,然后调用lock.wait()方法,交出锁的控制权,进入wait状态
  3. 对于线程B而言,由于A最开始获得锁,导致线程B无法运行,直到A调用lock.wait()方法释放控制权后,线程B才能得到锁
  4. 线程B在获得锁后打印1,2,3,然后调用lock.notify()方法,唤醒正在wait的A
  5. 线程A被唤醒后,继续打印剩余的2,3

为了更好的理解,我们现在修改上面的代码加上相关的日志方便理想,代码如下:

package com.test.thread;

public class ThreadDemo3 {

    public ThreadDemo3() {
        // TODO Auto-generated constructor stub
    }

    
    private static void demo3() {
        Object lock=new Object();
        Thread A=new Thread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("INFO: A 等待锁");
                synchronized(lock) {
                   System.out.println("INFO: A 得到锁 lock");
                   System.out.println("A1");
                   try {
                       System.out.println("INFO: A 准备进入等待状态,放弃锁");
                       lock.wait();
                 
                   }catch(InterruptedException e) {
                      e.printStackTrace();
                   }
                }
                System.out.println("INFO: 有人唤醒了A,A重新获得锁 lock");
                System.out.println("A2");
                System.out.println("A3");
       
            }
       
        });
        Thread B=new Thread(new Runnable() {
       
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("INFO: B 等待锁");
                synchronized(lock) {
                    System.out.println("INFO: B 获得了锁 lock");
                    System.out.println("B1");
                    System.out.println("B2");
                    System.out.println("B3");
                    System.out.println("INFO: B 打印完毕,调用notify方法唤醒A");
                    lock.notify();
                }
            }
            
        });
        A.start();
        B.start();
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        demo3();
    }

}

运行结果如下:

INFO: A 等待锁
INFO: A 得到锁 lock
INFO: B 等待锁
A1
INFO: A 准备进入等待状态,放弃锁
INFO: B 获得了锁 lock
B1
B2
B3
INFO: B 打印完毕,调用notify方法唤醒A
INFO: 有人唤醒了A,A重新获得锁 lock
A2
A3

四个线程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 countDownLatch=new CountdownLatch(2);
  2. 在等待线程里面调用countDownLatch.await()方法,进入等待状态,直到计数器数据变成0;
  3. 在其他线程里面,调用countDownLatch.countDown()方法,该方法会将计数器数值减小1;
  4. 当其他线程的countDown()方法把计数器数值变成0时,等待线程里面的countDownLatch.await()立即退出,继续执行下面的代码

代码实现如下:

package com.test.thread;

import java.util.concurrent.CountDownLatch;

public class ThreadCountDownLatch {

    public ThreadCountDownLatch() {
        // TODO Auto-generated constructor stub
    }
   
    private static void runDAfterABC() {
        int worker=3;
        CountDownLatch countDownLatch=new CountDownLatch(worker);
        new Thread(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("D is waiting for other thress thread");
                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() {
                    // TODO Auto-generated method stub
                    System.out.println(tN+" is working");
                    try {
                        Thread.sleep(100);
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(tN+" finished");
                    countDownLatch.countDown();
                }
                
            }).start();
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        runDAfterABC();

    }

}

运行结果如下:

D is waiting for other thress thread
A is working
B is working
C is working
A finished
B finished
C finished
All done,D starts working

其实说简单点,CountDownLatch 就是一个倒计数器,我们把初始化计数值设置成3,当D运行的时候,先调用countDownLatch.await()检查计数器数值是否为0,若不为0则保持等待状态;当A B C各自运行完毕都会利用countDownLatch.countDown(),将倒减计数器减1

当三个线程都运行完成后,计数器的值为0,此时此刻触发线程D的await()运行结束,进行向下运行,因此,CountDownLatch 适用于一个线程去等待多个线程的情况

三个运动员各自准备,等到三个人都准备好后,再一起跑

上面是一个形象的比喻,针对线程A B C 各自开始准备,直到三个线程都准备完毕,然后再同时运行,也就是要实现一种线程之间想关等待的效果,那么这种情况应该如何实现?

为了实现线程间相互等待这种需求,我们可以利用CyclicBarrier数据结构,它的基本用法如下:

  1. 先创建一个公共CyclicBarrier对象,设置同时等待的线程数,CyclicBarrier cyclicBarrier=new CyclicBarrier(3);
  2. 这些线程同时开始自己做准备,自身准备完毕后,需要等待别人准备完毕,这时调用cyclicBarrider.await();即可开始等待别人
  3. 当指定的同时等待的线程数都调用了cycliBarrier.await()时,意味着这些线程都准备完毕,然后这些线程才同时继续运行

代码实现如下:

package com.test.thread;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {

    public CyclicBarrierDemo() {
        // TODO Auto-generated constructor stub
    }

    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() {
                    // TODO Auto-generated method stub
                    long prepareTime=random.nextInt(10000)+100;
                    System.out.println(rN+" is preparing for time:"+prepareTime);
                    try {
                        Thread.sleep(prepareTime);
                    }catch(InterruptedException 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();
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
      runABCWhenAllReady();
    }

}

运行结果如下:

B is preparing for time:2555
A is preparing for time:8147
C is preparing for time:849
C is prepared waiting for others
B is prepared waiting for others
A is prepared waiting for others
A starts running 
C starts running 
B starts running 

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

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

我们一般会把Runnable对象传给Thread 去执行,可以看到run()在执行完成后不会返回任何结果,那么如果希望返回结果,这里可以利用一个类似的接口Callable来实现,Callable最大的区别就是返回泛型V结果

那么下一个问题就是,如果把子线程的结果回传给主线程?在Java中有一个类是配合Callable使用的:FutureTask,需要注意的是,它获取结果的方法get会阻塞主线程

举例说明,我们想让子线程去计算1加到100,并把结果返回给主线程,代码实现如下:

package com.test.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class FutureTaskDemo {

    public FutureTaskDemo() {
        // TODO Auto-generated constructor stub
    }

    private static void doTaskWithResultInWorker() {
        Callable<Integer> callable=new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                // TODO Auto-generated method stub
                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<Integer>(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(Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
       doTaskWithResultInWorker();
    }

}

代码运行结果如下:

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

从运行结果可以看到,主线程调用futureTask.get()方法时阻塞主线程;然后Callable内部开始执行,并返回计算结果;此时futureTask.get()得到结果,主线程恢复运行

通过上述代码我们可以学到,通过FutureTask和Callable可以直接在主线程里面获得子线程的运算结果,只不过需要阻塞主线程,当然,如果不希望阻塞主线程,可以考虑利用ExecutorService,把FutureTask放到线程池去管理执行

posted on 2018-11-08 15:40  anqli_java  阅读(314)  评论(0编辑  收藏  举报