线程通信方式有几种(实现线程间通信的几种方法)

线程通信方式有几种(实现线程间通信的几种方法)

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

本文涉及到的知识点:

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

本文涉及代码:https://github.com/wingjay/HelloJava/blob/master/multi-thread/src/ForArticle.java

下面我从几个例子作为切入点来讲解下 Java 里有哪些方法来实现线程间通信。

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

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

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

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

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

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

假设有两个线程,一个是线程 A,另一个是线程 B,两个线程分别依次打印 1-3 三个数字即可。我们来看下代码:

privatestaticvoiddemo1(){ThreadA=newThread(newRunnable(){@Overridepublicvoidrun(){printNumber(“A”);}});ThreadB=newThread(newRunnable(){@Overridepublicvoidrun(){printNumber(“B”);}});A.start();B.start();}

其中的 printNumber(String) 实现如下,用来依次打印 1, 2, 3 三个数字:

privatestaticvoidprintNumber(StringthreadName){inti=0;while(i <3){try{Thread.sleep(100);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(threadName “print:” i);}}

这时我们得到的结果是:

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

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

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

privatestaticvoiddemo2(){ThreadA=newThread(newRunnable(){@Overridepublicvoidrun(){printNumber(“A”);}});ThreadB=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println(“B开始等待A”);try{A.join();}catch(InterruptedExceptione){e.printStackTrace();}printNumber(“B”);}});B.start();A.start();}

得到的结果如下:

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

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

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

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

还是上面那个例子,我现在希望 A 在打印完 1 后,再让 B 打印 1, 2, 3,最后再回到 A 继续打印 2, 3。这种需求下,显然 Thread.join() 已经不能满足了。我们需要更细粒度的锁来控制执行顺序。推荐:Java面试练题宝典

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

/***A1,B1,B2,B3,A2,A3*/privatestaticvoiddemo3(){Objectlock=newObject();ThreadA=newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(lock){System.out.println(“A1”);try{lock.wait();}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(“A2”);System.out.println(“A3”);}}});ThreadB=newThread(newRunnable(){@Overridepublicvoidrun(){synchronized(lock){System.out.println(“B1”);System.out.println(“B2”);System.out.println(“B3”);lock.notify();}}});A.start();B.start();}

打印结果如下:

A 1A waiting…

B 1B 2B 3A 2A 3

正是我们要的结果。

那么,这个过程发生了什么呢?

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

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

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

B 在得到锁后打印 1, 2, 3;然后调用 lock.notify() 方法,唤醒正在 wait 的 A;

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

为了更好理解,我在上面的代码里加上 log 方便读者查看。

privatestaticvoiddemo3(){Objectlock=newObject();ThreadA=newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println(“INFO:A等待锁”);synchronized(lock){System.out.println(“INFO:A得到了锁lock”);System.out.println(“A1”);try{System.out.println(“INFO:A准备进入等待状态,放弃锁lock的控制权”);lock.wait();}catch(InterruptedExceptione){e.printStackTrace();}System.out.println(“INFO:有人唤醒了A,A重新获得锁lock”);System.out.println(“A2”);System.out.println(“A3”);}}});ThreadB=newThread(newRunnable(){@Overridepublicvoidrun(){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方法”);lock.notify();}}});A.start();B.start();}

打印结果如下:

INFO: A 等待锁INFO: A 得到了锁 lockA 1INFO: A 准备进入等待状态,调用 lock.wait() 放弃锁 lock 的控制权INFO: B 等待锁INFO: B 得到了锁 lockB 1B 2B 3INFO: B 打印完毕,调用 lock.notify() 方法INFO: 有人唤醒了 A, A 重新获得锁 lockA 2A 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 来实现这类通信方式。它的基本用法是:

创建一个计数器,设置初始值,CountdownLatch countDownLatch = new CountDownLatch(2);

在 等待线程 里调用 countDownLatch.await() 方法,进入等待状态,直到计数值变成 0;

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

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

实现代码如下:

privatestaticvoidrunDAfterABC(){intworker=3;CountDownLatchcountDownLatch=newCountDownLatch(worker);newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println(“Diswaitingforotherthreethreads”);try{countDownLatch.await();System.out.println(“Alldone,Dstartsworking”);}catch(InterruptedExceptione){e.printStackTrace();}}}).start();for(charthreadName=’A’;threadName<=’C’;threadName ){finalStringtN=String.valueOf(threadName);newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println(tN “isworking”);try{Thread.sleep(100);}catch(Exceptione){e.printStackTrace();}System.out.println(tN “finished”);countDownLatch.countDown();}}).start();}}

下面是运行结果:

D is waiting for other three threadsA is workingB is workingC is working

A finishedC finishedB finishedAll done, D starts working

其实简单点来说,CountDownLatch 就是一个倒计数器,我们把初始计数值设置为3,当 D 运行时,先调用 countDownLatch.await() 检查计数器值是否为 0,若不为 0 则保持等待状态;当A B C 各自运行完后都会利用countDownLatch.countDown(),将倒计数器减 1,当三个都运行完后,计数器被减至 0;此时立即触发 D 的 await() 运行结束,继续向下执行。推荐:Java面试练题宝典

因此,CountDownLatch 适用于一个线程去等待多个线程的情况。

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

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

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

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

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

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

当指定的 同时等待 的线程数都调用了 cyclicBarrier.await();时,意味着这些线程都准备完毕好,然后这些线程才 同时继续执行。

实现代码如下,设想有三个跑步运动员,各自准备好后等待其他人,全部准备好后才开始跑:

privatestaticvoidrunABCWhenAllReady(){intrunner=3;CyclicBarriercyclicBarrier=newCyclicBarrier(runner);finalRandomrandom=newRandom();for(charrunnerName=’A’;runnerName<=’C’;runnerName ){finalStringrN=String.valueOf(runnerName);newThread(newRunnable(){@Overridepublicvoidrun(){longprepareTime=random.nextInt(10000) 100;System.out.println(rN “ispreparingfortime:” prepareTime);try{Thread.sleep(prepareTime);}catch(Exceptione){e.printStackTrace();}try{System.out.println(rN “isprepared,waitingforothers”);cyclicBarrier.await();//当前运动员准备完毕,等待别人准备好}catch(InterruptedExceptione){e.printStackTrace();}catch(BrokenBarrierExceptione){e.printStackTrace();}System.out.println(rN “startsrunning”);//所有运动员都准备好了,一起开始跑}}).start();}}

打印的结果如下:

A is preparing for time: 4131B is preparing for time: 6349C is preparing for time: 8206A is prepared, waiting for othersB is prepared, waiting for othersC is prepared, waiting for othersC starts runningA starts runningB starts running

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

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

回顾线程的创建,我们一般会把 Runnable 对象传给 Thread 去执行。Runnable定义如下:

publicinterfaceRunnable{publicabstractvoidrun();}

可以看到 run() 在执行完后不会返回任何结果。那如果希望返回结果呢?这里可以利用另一个类似的接口类 Callable:

@FunctionalInterfacepublicinterfaceCallable<V>{/***Computesaresult,orthrowsanexceptionifunabletodoso.**@returncomputedresult*@throwsExceptionifunabletocomputearesult*/Vcall()throwsException;}

可以看出 Callable 最大区别就是返回范型 V 结果。

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

举例,我们想让子线程去计算从 1 加到 100,并把算出的结果返回到主线程。

privatestaticvoiddoTaskWithResultInWorker(){Callable<Integer>callable=newCallable<Integer>(){@OverridepublicIntegercall()throwsException{System.out.println(“Taskstarts”);Thread.sleep(1000);intresult=0;for(inti=0;i<=100;i ){result =i;}System.out.println(“Taskfinishedandreturnresult”);returnresult;}};FutureTask<Integer>futureTask=newFutureTask<>(callable);newThread(futureTask).start();try{System.out.println(“BeforefutureTask.get()”);System.out.println(“Result:” futureTask.get());System.out.println(“AfterfutureTask.get()”);}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();}}

打印结果如下:

Before futureTask.get()Task startsTask finished and return resultResult: 5050After futureTask.get()

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

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

小结

多线程是现代语言的共同特性,而线程间通信、线程同步、线程安全是很重要的话题。本文针对 Java 的线程间通信进行了大致的讲解,后续还会对线程同步、线程安全进行讲解。

推荐好文

分享一套基于SpringBoot和Vue的企业级中后台开源项目,代码很规范!

能挣钱的,开源 SpringBoot 商城系统,功能超全,超漂亮!

发表评论

登录后才能评论