Java中实现线程间通信的实例教程

目录
  • 前言
  • 1. 如何让两个线程依次执行?
  • 2. 如何让两个线程按照指定的方式有序相交?
  • 3. 线程 D 在A、B、C都同步执行完毕后执行
  • 4. 三个运动员分开准备同时开跑
  • 5. 子线程将结果返回给主线程
  • 总结

前言

虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信。
关于线程间通信本文涉及到的方法和类包括:thread.join()、object.wait()、object.notify()、CountdownLatch、CyclicBarrier、FutureTask、Callable。

接下来将用几个例子来介绍如何在Java中实现线程间通信:

  1. 如何让两个线程依次执行,即一个线程等待另一个线程执行完成后再执行?
  2. 如何让两个线程以指定的方式有序相交执行?
  3. 有四个线程:A、B、C、D,如何实现 D 在 A、B、C 都同步执行完毕后执行?
  4. 三个运动员分开准备,然后在每个人准备好后同时开始跑步。
  5. 子线程完成任务后,将结果返回给主线程。

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

假设有两个线程:A 和 B,这两个线程都可以按照顺序打印数字,代码如下:

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

}

得到的结果如下:

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

可以看到 A 和 B 同时打印数字,如果我们希望 B 在 A 执行完成之后开始执行,那么可以使用 thread.join() 方法实现,代码如下:

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

得到的结果如下:

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

我们可以看到该 a.join() 方法会让 B 等待 A 完成打印。

thread.join() 方法的作用就是阻塞当前线程,等待调用 join() 方法的线程执行完毕后再执行后面的代码。

查看 join() 方法的源码,内部是调用了 join(0) ,如下:

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

查看 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;
        }
    }
}

从源码中可以看出 join(long millis) 方法是通过 wait(long timeout) (Object 提供的方法)方法实现的,调用 wait 方法之前,当前线程必须获得对象的锁,所以此 join 方法使用了 synchronized 加锁,锁对象是线程的实例对象。其中 wait(0)方法会让当前线程阻塞等待,直到另一个线程调用此对象的 notify() 或者 notifyAll() 方法才会继续执行。当调用 join 方法的线程结束的时候会调用 notifyAll() 方法,所以 join() 方法可以实现一个线程等待另一个调用 join() 的线程结束后再执行。

虚假唤醒:一个线程在没有被通知、中断、超时的情况下被唤醒;
虚假唤醒可能导致条件不成立的情况下执行代码,破坏被锁保护的约束关系;
为什么使用 while 循环来避免虚假唤醒:
在 if 块中使用 wait 方法,是非常危险的,因为一旦线程被唤醒,并得到锁,就不会再判断 if 条件而执行 if 语句块外的代码,所以建议凡是先要做条件判断,再 wait 的地方,都使用 while 循环来做,循环会在等待之前和之后对条件进行测试。

2. 如何让两个线程按照指定的方式有序相交?

如果现在我们希望 B线程在 A 线程打印 1 后立即打印 1,2,3,然后 A 线程继续打印 2,3,那么我们需要更细粒度的锁来控制执行顺序。

在这里,我们可以利用 object.wait() 和 object.notify() 方法,代码如下:

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

得到的结果如下:

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

上述代码的执行流程如下:

  1. 首先我们创建一个由 A 和 B 共享的对象锁: lock = new Object();
  2. 当A拿到锁时,先打印1,然后调用lock.wait()方法进入等待状态,然后交出锁的控制权;
  3. B 不会被执行,直到 A 调用该lock.wait()方法释放控制权并且 B 获得锁;
  4. B拿到锁后打印1,2,3,然后调用lock.notify()方法唤醒正在等待的A;
  5. A 唤醒后继续打印剩余的 2,3。

为了便于理解,我将上面的代码添加了日志,代码如下:

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

得到的结果如下:

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. 线程 D 在A、B、C都同步执行完毕后执行

thread.join() 前面介绍的方法允许一个线程在等待另一个线程完成运行后继续执行。但是如果我们将A、B、C依次加入到D线程中,就会让A、B、C依次执行,而我们希望它们三个同步运行。

我们要实现的目标是:A、B、C三个线程可以同时开始运行,各自独立运行完成后通知D;D 不会开始运行,直到 A、B 和 C 都运行完毕。所以我们 CountdownLatch 用来实现这种类型的通信。它的基本用法是:

  1. 创建一个计数器,并设置一个初始值, CountdownLatch countDownLatch = new CountDownLatch(3);
  2. 调用countDownLatch.await()进入等待状态,直到计数值变为0;
  3. 在其他线程调用countDownLatch.countDown(),该方法会将计数值减一;
  4. 当计数器的值变为 0 时,countDownLatch.await()等待线程中的方法会继续执行下面的代码。

实现代码如下:

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

得到的结果如下:

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

其实CountDownLatch它本身就是一个倒数计数器,我们把初始的count值设置为3。D运行的时候,首先调用该countDownLatch.await()方法检查计数器的值是否为0,如果不是0则保持等待状态. A、B、C 运行完毕后,分别使用countDownLatch.countDown()方法将倒数计数器减1。计数器将减为 0,然后通知await()方法结束等待,D开始继续执行。
因此,CountDownLatch适用于一个线程需要等待多个线程的情况。

4. 三个运动员分开准备同时开跑

这一次,A、B、C这三个线程都需要分别准备,等三个线程都准备好后开始同时运行,我们应该如何做到这一点?
CountDownLatch可以用来计数,但完成计数的时候,只有一个线程的一个await()方法会得到响应,所以多线程不能在同一时间被触发。为了达到线程相互等待的效果,我们可以使用该CyclicBarrier,其基本用法为:

  1. 首先创建一个公共对象CyclicBarrier,并设置同时等待的线程数,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
  2. 这些线程同时开始准备,准备好后,需要等待别人准备好,所以调用cyclicBarrier.await()方法等待别人;
  3. 当指定的需要同时等待的线程都调用了该cyclicBarrier.await()方法时,意味着这些线程准备好了,那么这些线程就会开始同时继续执行。

想象一下有三个跑步者需要同时开始跑步,所以他们需要等待其他人都准备好,实现代码如下:

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

得到结果如下:

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

CyclicBarrier 的作用就是等待多个线程同时执行。

5. 子线程将结果返回给主线程

在实际开发中,往往我们需要创建子线程来做一些耗时的任务,然后将执行结果传回主线程。那么如何在 Java 中实现呢?

一般在创建线程的时候,我们会把 Runnable 对象传递给 Thread 执行,Runable 的源码如下:

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

可以看到 Runable 是一个函数式接口,该接口中的 run 方法没有返回值,那么如果要返回结果,可以使用另一个类似的接口 Callable。

函数式接口:只有一个方法的接口

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在于它返回的是泛型。

那么接下来的问题是,如何将子线程的结果传回去呢?Java 有一个类,FutureTask,它可以与 一起工作Callable,但请注意,get用于获取结果的方法会阻塞主线程。FutureTask 本质上还是一个 Runnable,所以可以直接传到 Thread 中。

比如我们想让子线程计算1到100的总和,并将结果返回给主线程,代码如下:

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

得到的结果如下:

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

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

在这里我们可以了解到,FutureTask和Callable可以直接在主线程中获取子线程的结果,但是它们会阻塞主线程。当然,如果你不希望阻塞主线程,可以考虑使用ExecutorService把FutureTask到线程池来管理执行。

参考文章:

www.tutorialdocs.com/article/jav

总结

到此这篇关于Java中实现线程间通信的文章就介绍到这了,更多相关Java线程间通信内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入理解JAVA多线程之线程间的通信方式

    一,介绍 本总结我对于JAVA多线程中线程之间的通信方式的理解,主要以代码结合文字的方式来讨论线程间的通信,故摘抄了书中的一些示例代码. 二,线程间的通信方式 ①同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信. 参考示例: public class MyObject { synchronized public void methodA() { //do something.... } synchronized public void methodB()

  • Java线程间的通信方式详解

    本总结我对于JAVA多线程中线程之间的通信方式的理解,主要以代码结合文字的方式来讨论线程间的通信,故摘抄了书中的一些示例代码,具体内容如下 ①同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信. 参考示例: public class MyObject { synchronized public void methodA() { //do something.... } synchronized public void methodB() { //do so

  • Java多线程中线程间的通信实例详解

    Java多线程中线程间的通信 一.使用while方式来实现线程之间的通信 package com.ietree.multithread.sync; import java.util.ArrayList; import java.util.List; public class MyList { private volatile static List list = new ArrayList(); public void add() { list.add("apple"); } publ

  • java线程间通信的通俗解释及代码示例

    线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度). 进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统.以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制.这样进程间的通信就不局限于单台计算机了,实现了网络通信.线程通信主要分为以下几个部分,下面通过生活中图书馆借书的例子简单讲解以下: 通过共享对象通信 加入图书馆只有

  • 详谈java线程与线程、进程与进程间通信

    线程与线程间通信 一.基本概念以及线程与进程之间的区别联系: 关于进程和线程,首先从定义上理解就有所不同 1.进程是什么? 是具有一定独立功能的程序.它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独 立运行的一段程序. 2.线程又是什么? 线程进程的一个实体,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源. 在运行时,只是暂用一些计数器.寄存器和栈 . 他们之间的关系 1.一个线程只能属于一个进程,而一个

  • 浅析Java中如何实现线程之间通信

    正常情况下,每个子线程完成各自的任务就可以结束了.不过有的时候,我们希望多个线程协同工作来完成某个任务,这时就涉及到了线程间通信了. 本文涉及到的知识点:thread.join(), object.wait(), object.notify(), CountdownLatch, CyclicBarrier, FutureTask, Callable 等. 下面我从几个例子作为切入点来讲解下 Java 里有哪些方法来实现线程间通信. 如何让两个线程依次执行? 那如何让两个线程按照指定方式有序交叉运

  • java多线程编程学习(线程间通信)

    一.概要 线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体,线程间的通信就是成为整体的必用方案之一.可以说,使线程进行通信后,系统之间的交互性会更强大,在大大提高cpu利用率的同时还会使程序员对各线程任务在处理过程中进行有效的把控和监督. 二.等待/通知机制 1."wait/notify"机制:等待/通知机制,wait使线程暂停运行,而notify 使暂停的线程继续运行.用一个厨师和服务员的交互来说明: (1) 服务员取到菜的时间取决于厨师,所以服务员就有&

  • 深入解析Java的线程同步以及线程间通信

    Java线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization).像你所看到的,Java为此提供了独特的,语言水平上的支持. 同步的关键是管程(也叫信号量semaphore)的概念.管程是一个互斥独占锁定的对象,或称互斥体(mutex).在给定的时间,仅有一个线程可以获得管程.当一个线程需要锁定,它必须进入管程.所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程.这些其他的线程被

  • java多线程实现服务器端与多客户端之间的通信

    用java语言构建一个网络服务器,实现客户端和服务器之间通信,实现客户端拥有独立线程,互不干扰. 应用多线程来实现服务器与多线程之间的通信的基本步骤 服务器端创建ServerSocket,循环调用accept()等待客户端链接 客户端创建一个Socket并请求和服务器端链接 服务器端接受客户端请求,创建socekt与该客户端建立专线链接 建立链接的socket在一个单独的线程上对话 服务器继续等待新的链接 服务器端Server.java package test.concurrent.socke

  • Java中实现线程间通信的实例教程

    目录 前言 1. 如何让两个线程依次执行? 2. 如何让两个线程按照指定的方式有序相交? 3. 线程 D 在A.B.C都同步执行完毕后执行 4. 三个运动员分开准备同时开跑 5. 子线程将结果返回给主线程 总结 前言 虽然通常每个子线程只需要完成自己的任务,但是有时我们希望多个线程一起工作来完成一个任务,这就涉及到线程间通信. 关于线程间通信本文涉及到的方法和类包括:thread.join().object.wait().object.notify().CountdownLatch.Cyclic

  • Java编程之多线程死锁与线程间通信简单实现代码

    死锁定义 死锁是指两个或者多个线程被永久阻塞的一种局面,产生的前提是要有两个或两个以上的线程,并且来操作两个或者多个以上的共同资源:我的理解是用两个线程来举例,现有线程A和B同时操作两个共同资源a和b,A操作a的时候上锁LockA,继续执行的时候,A还需要LockB进行下面的操作,这个时候b资源在被B线程操作,刚好被上了锁LockB,假如此时线程B刚好释放了LockB则没有问题,但没有释放LockB锁的时候,线程A和B形成了对LockB锁资源的争夺,从而造成阻塞,形成死锁:具体其死锁代码如下:

  • Java线程间通信不同步问题原理与模拟实例

    本文实例讲述了Java线程间通信不同步问题原理与模拟.分享给大家供大家参考,具体如下: 一 点睛 下面两种情况可造成线程间不同步: 1 生产者没生产完,消费者就来消费. 2 消费者没消费完,生产者又来生产,覆盖了还没来得及消费的数据. 二 代码 class Producer implements Runnable { private Person person = null; public Producer( Person person ) { this.person = person; } @

  • 浅谈Java线程间通信之wait/notify

    Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.先来我们来看下相关定义: wait() :调用该方法的线程进入WATTING状态,只有等待另外线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁. wait(long):超时等待最多long毫秒,如果没有通知就超时返回. notify() :通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提

  • Java通过wait()和notifyAll()方法实现线程间通信

    本文实例为大家分享了Java实现线程间通信的具体代码,供大家参考,具体内容如下 Java代码(使用了2个内部类): package Threads; import java.util.LinkedList; /** * Created by Frank */ public class ProdCons { protected LinkedList<Object> list = new LinkedList<>(); protected int max; protected bool

  • Java编程线程间通信与信号量代码示例

    1.信号量Semaphore 先说说Semaphore,Semaphore可以控制某个资源可被同时访问的个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可.一般用于控制并发线程数,及线程间互斥.另外重入锁ReentrantLock也可以实现该功能,但实现上要复杂些. 功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了.另外等待的

  • Java使用wait/notify实现线程间通信上篇

    线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一,使用线程间进行通信后,系统之间的交互性会更强大,大大提高CPU利用率. 等待/通知机制 (1)不使用等待/通知机制实现线程间通信 样例代码如下: public class TestC2 { public static void main(String[] args) throws Exception { MyList myList = new MyList(); ThreadA

随机推荐