如何在Java中实现线程间通信

本文深入探讨了Java中线程间通信的多种方法,包括使用join(), wait()和notify(), CountdownLatch, CyclicBarrier以及FutureTask和Callable。通过具体示例,详细讲解了如何控制线程执行顺序,实现线程交叉执行,以及子线程如何向主线程返回结果。

如何在Java中实现线程间通信

虽然通常每个子线程只需要完成自己的任务,但有时我们可能希望多个线程一起工作来完成一个任务,这涉及到线程间的通信。

将使用几个示例来解释如何在Java中实现线程间通信:
  • 如何使两个线程按照顺序执行?
  • 如何使两个线程以指定的方式有序交叉
  • 有四个线程:A、B、C、D, D在A、B和C同步执行完成后执行
  • 三名运动员分开准备,然后他们在每个运动员准备好后开始跑步
  • 子线程完成任务后,它会将结果返回给主线程

一、如何使两个线程按照顺序执行?

假设有两个线程:线程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(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(threadName + ": print:" + i);
	}
}

我们得到的结果是:

A: print:1 
B: 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,等B打印完成,继续打印A的2、3。 可以利用 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
B---1
B---2
B---3
A---2
A---3

  • 首先,我们创建一个由A和B共享的对象锁: Object lock = new Object();
  • 当A获得锁定时,它首先打印1,然后调用 lock.wait();方法使其进入等待状态,然后移交锁定控制
  • 在A调用 lock.wait();释放控制的方法并且B获得锁定之前,B不会被执行
  • B在获得锁定后打印1、2、3,然后调用lock.notify();方法唤醒正在等待的A
  • A被唤醒后继续打印剩余的2、3

修改demo3方法,加入日志添加到上面的代码中,以便更容易理解。

private static void demo3(){
	Object lock = new Object();
	
	Thread A = new Thread(new Runnable() {
		@Override
		public void run() {
			System.out.println("A正在等待锁 ");
			synchronized (lock) {
				System.out.println("A得到锁");
				System.out.println("A---1");
				try {
					System.out.println("A准备进入等待状态,放弃对锁的控制");
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("B唤醒A,A重新得到锁");
				System.out.println("A---2");
				System.out.println("A---3");
			}
		}
	});
	
	
	Thread B = new Thread(new Runnable(){
		@Override
		public void run() {
			System.out.println("B正在等待锁 ");
			synchronized (lock) {
				System.out.println("B---1");
				System.out.println("B---2");
				System.out.println("B---3");
				System.out.println("B打印结束,准备调用lock.notify()");
				lock.notify();
			}
		}
	});
	
	A.start();
	B.start();
}

结果如下:

A正在等待锁 
A得到锁
A---1
A准备进入等待状态,放弃对锁的控制
B正在等待锁 
B---1
B---2
B---3
B打印结束,准备调用lock.notify()
B唤醒A,A重新得到锁
A---2
A---3

三、D在A、B和C同步执行完成后执行?

前面介绍thread.join()方法允许一个线程在等待另一个线程完成后继续执行。但是我们如果将A、B、C有序的连接到D线程中,将会依次执行A、B、C,我们是希望它们三个同步允许。 要实现的目标:三个线程A、B、C同时开始运行,每个线程在完成独立运行后通知D。在A、B、C完成之前,D不会执行。所以我们用CountdownLatch来实现这种类型的通信。它的基本用法:

  • 创建一个计数器,并且设置一个初始值,CountDownLatch countDownLatch = new CountDownLatch(3);
  • 在等待线程中调用countDownLatch.wait();,进入等待状态,直到计数值变为0
  • 在其他线程中调用countDownLatch.countDown();,计数值减1
  • countDown()其他线程中的方法将计数值变为0时,countDownLatch.wait();等待线程中的方法将立即退出并且继续执行

代码如下:

private static void demo4(){
	int woeker = 3;
	CountDownLatch countDownLatch = new CountDownLatch(woeker);
	
	Thread D = new Thread(new Runnable(){
		@Override
		public void run() {
			System.out.println("D正在等待其他三个线程");
			try {
				countDownLatch.await();
				System.out.println("全部完成,D开始执行");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	});
	
	D.start();
	
	for(char threadName='A';threadName<='C';threadName++){
		final String tName = String.valueOf(threadName);
		new Thread(new Runnable(){
			@Override
			public void run() {
				System.out.println("开始执行线程:"+tName);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(tName+"执行完成");
				countDownLatch.countDown();
			}
		}).start();
	} // for(char threadName='A';threadName<='C';threadName++)
}

执行结果:

D正在等待其他三个线程
开始执行线程:A
开始执行线程:C
开始执行线程:B
C执行完成
B执行完成
A执行完成
全部完成,D开始执行

实际上CountDownLatch本身就是计时器,我们将初始计数值设置为3,当D运行时,它首先会调用countDownLatch.await();来检查计数器值是否为0,如果不为0,它将保持等待状态,A、B、C各自调用countDownLatch.countDown();,在完成后将计数器递减1。当这三个完成时,计数器的值为0,然后D中的countDownLatch.await();被触发,开始执行D。 因此CountDownLatch适用于一个线程需要等待多个线程的情况。

四、3名运动员准备跑步

三名运动员分开准备,然后他们在每个运动员准备好后开始跑步。 三个线程A、B、C中的每一个都需要单独准备,然后他们三个都准备好后,开始同时执行。 在上面说CountDownLatch计时器,完成计数的时候,只有一个线程的一个await()方法会得到响应,所以多线程不能再同一时间被触发。 为了实现多线程相互等待的效果,我们可以使用CyclicBarrier,基本用法:

  • 首先创建一个公共对象CyclicBarrier,并且设置同时等待的线程数,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
  • 这些线程开始同时准备,准备好之后,他们需要等待其它线程完成准备,cyclicBarrier.await();等待其他人
  • 当等待指定的线程调用cyclicBarrier.await();时,就说明这些线程已经准备就绪,那么这些线程将开始同时继续执行。

代码如下:

private static void demo5(){
	int runner = 3;
	CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);
	final Random random = new Random();
	
	for(char runnerName = 'A'; runnerName <= 'C'; runnerName++){
		final String rName = String.valueOf(runnerName);
		
		new Thread(new Runnable(){
			[@Override](https://my.oschina.net/u/1162528)
			public void run() {
				long prepareTime = random.nextInt(10000) + 100;
				System.out.println(rName + ":准备的时间:" + prepareTime);
				try {
					Thread.sleep(prepareTime);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(rName + "准备完毕,等待其他");
				try {
					cyclicBarrier.await();
				} catch (InterruptedException | BrokenBarrierException e) {
					e.printStackTrace();
				}
				//所有人都准备完成
				System.out.println(rName + "开始执行");
			}
		}).start();
	}
}

结果如下:

A:准备的时间:6960
C:准备的时间:1079
B:准备的时间:2713
C准备完毕,等待其他
B准备完毕,等待其他
A准备完毕,等待其他
A开始执行
C开始执行
B开始执行

五、子线程完成任务后,将结果返回给主线程

在实际开发中,我们经常需要创建子线程来执行一些耗时的任务。然后将执行结果传递给主线程。 在创建线程时,通常我们会使用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](https://my.oschina.net/u/556800) computed result
	 * [@throws](https://my.oschina.net/throws) Exception if unable to compute a result
	 */
	V call() throws Exception;
}

如何将子线程的结果传回去? 例如,子线程计算从1到100的总和,并将结果返回给主线程。

private static void demo6(){
	
	Callable<Integer> callable = new Callable<Integer>(){
		[@Override](https://my.oschina.net/u/1162528)
		public Integer call() throws Exception {
			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()之前");
		System.out.println("Result:" + futureTask.get());
		System.out.println("主线程调用 futureTask.get()之后");
	} catch (InterruptedException | ExecutionException e) {
		e.printStackTrace();
	}
}

结果如下:

调用 futureTask.get()之前
任务开始
任务结束并且返回结果
Result:5050
调用 futureTask.get()之后

可以看出当主线程调用futureTask.get()方法时,它会阻塞主线程,然后Callable开始在内部执行,并且返回操作结果,然后futureTask.get()获取结果,主线程恢复运行。

在这里我们了解到FutureTaskCallable可以直接在主线程中获取子线程的结果,但是他们会阻塞主线程,如果你不想阻止主线程,可以考虑使用ExecutorService线程池来管理执行。

本文参考:https://www.tutorialdocs.com/article/java-inter-thread-communication.html

转载于:https://my.oschina.net/lwenhao/blog/3016677

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值