如何实现线程间的通讯(转载)

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

本文涉及到的知识点: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 三个数字即可。我们来看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 三个数字:

1
2
3
4
5
6
7
8
9
10
11
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);
    }
}

这时我们得到的结果是:

1
2
3
4
5
6
B print: 1
A print: 1
B print: 2
A print: 2
B print: 3
A print: 3

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}

得到的结果如下:

1
2
3
4
5
6
7
8
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() 两个方法来实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * A 1, B 1, B 2, B 3, A 2, A 3
 */
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();
}

打印结果如下:

1
2
3
4
5
6
7
8
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。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static void demo3() {
    Object lock = new Object();
    Thread A = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("INFO: A 等待锁");
            synchronized (lock) {
                System.out.println("INFO: A 得到了锁 lock");
                System.out.println("A 1");
                try {
                    System.out.println("INFO: A 准备进入等待状态,放弃锁 lock 的控制权");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("INFO: 有人唤醒了 A, A 重新获得锁 lock");
                System.out.println("A 2");
                System.out.println("A 3");
            }
        }
    });
    Thread B = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("INFO: B 等待锁");
            synchronized (lock) {
                System.out.println("INFO: B 得到了锁 lock");
                System.out.println("B 1");
                System.out.println("B 2");
                System.out.println("B 3");
                System.out.println("INFO: B 打印完毕,调用 notify 方法");
                lock.notify();
            }
        }
    });
    A.start();
    B.start();
}

打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
INFO: A 等待锁
INFO: A 得到了锁 lock
A 1
INFO: A 准备进入等待状态,调用 lock.wait() 放弃锁 lock 的控制权
INFO: B 等待锁
INFO: B 得到了锁 lock
B 1
B 2
B 3
INFO: B 打印完毕,调用 lock.notify() 方法
INFO: 有人唤醒了 A, A 重新获得锁 lock
A 2
A 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 countDownLatch = new CountDownLatch(2);
  2. 在 等待线程 里调用 countDownLatch.await() 方法,进入等待状态,直到计数值变成 0;
  3. 在 其他线程 里,调用 countDownLatch.countDown() 方法,该方法会将计数值减小 1;
  4. 当 其他线程 的 countDown() 方法把计数值变成 0 时,等待线程 里的 countDownLatch.await() 立即退出,继续执行下面的代码。

实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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();
    }
}

下面是运行结果:

1
2
3
4
5
6
7
8
9
D is waiting for other three threads
A is working
B is working
C is working
 
A finished
C finished
B 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 各自开始准备,直到三者都准备完毕,然后再同时运行 。也就是要实现一种 线程之间互相等待 的效果,那应该怎么来实现呢?

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();
    }
}

打印的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
B starts running

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

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

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

1
2
3
public interface Runnable {
    public abstract void run();
}

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

1
2
3
4
5
6
7
8
9
10
@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 使用的:FutureTask,不过注意,它获取结果的 get 方法会阻塞主线程。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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();
    }
}

打印结果如下:

1
2
3
4
5
6
7
Before futureTask.get()
 
Task starts
Task finished and return result
 
Result: 5050
After futureTask.get()

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

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

小结

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

 

转载于:https://www.cnblogs.com/ZhongKing/p/7694842.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/454793.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Doracle.jdbc.J2EE13Compliant=true

To make the Oracle driver behave in a Java EE-compliant manner, you must define the following JVM property: -Doracle.jdbc.J2EE13Complianttrue转载于:https://www.cnblogs.com/heben/p/7472989.html

Docker 精通之微服务

Docker 精通系列 Docker 精通之入门Docker 精通之微服务Docker 精通之常用命令Docker 精通之 Dockerfile Docker 是一个容器工具&#xff0c;提供虚拟环境。很多人认为&#xff0c;它改变了我们对软件的认识。 站在 Docker 的角度&#xff0c;软件就是容器的组合&#xff1a;业…

苦逼的程序员怎么发展

今天组长开会时跟我谈话了&#xff0c;说的意思是我更适合另外一个组&#xff0c;我现在这个组没有什么技术含量&#xff0c;另外一个组技术含量可能更深些。 苦逼啊&#xff0c;这世界缺少的都是耐心&#xff0c;老是用一成不变的模式看人&#xff0c;我是那么还和以前一样沉…

php根据浏览器调用支付_Android通过外部浏览器调用微信H5支付,Android+PHP详解

看了好多关于讲解微信H5支付开发的文章&#xff0c;大多数都是通过微信内部浏览器来调用支付接口(其实就是公众号支付)&#xff0c;可能是因为H5支付接口刚开放不久吧。微信官方体验链接&#xff1a;http://wxpay.wxutil.com/mch/pay/h5.v2.php&#xff0c;请在微信外浏览器打开…

秒杀系统架构分析与实战

0 系列目录 秒杀系统架构 秒杀系统架构分析与实战1 秒杀业务分析 正常电子商务流程 &#xff08;1&#xff09;查询商品&#xff1b;&#xff08;2&#xff09;创建订单&#xff1b;&#xff08;3&#xff09;扣减库存&#xff1b;&#xff08;4&#xff09;更新订单&#xff1…

Docker 精通之常用命令

Docker 精通系列 Docker 精通之入门Docker 精通之微服务Docker 精通之常用命令Docker 精通之 Dockerfile 配置文件路径 /etc/sysconfig/docker docker镜像及容器目录 主目录&#xff1a;/var/lib/docker 容器存放目录&#xff1a;/var/lib/docker/containers/ 镜像存放目…

世界500强面试题(经典)

本书中的问题&#xff0c;是软件业人士在面试时常问的&#xff0c;其中大多数出自微软公司&#xff0c;也有一些来自世界500强公司中的其 他公司。其实道理是相通的&#xff0c;认真领会&#xff0c;加以变通&#xff0c;当你读完全书时&#xff0c;你会发现你已经找到了一块进…

php 获取字符串中的url,php使用正则表达式获取字符串中的URL

今天写一个问答系统上线之后发现有很多人发链接了,由于业务部门要我们过滤掉网站地址了,下面我给大家分享一个提取字符串url地址函数,代码如下:$str 本文实例讲述了php匹配字符串里所有URL地址的方法。http://www.manongjc.com 分享给大家供大家参考;preg_match_all("/htt…

如何提高安卓代码的质量和语法

本文讲的是如何提高安卓代码的质量和语法&#xff0c;在这篇文章中&#xff0c;我会介绍几种不同的方式&#xff0c;让你通过自动化工具提高你的Android代码质量&#xff0c;包括 Checkstyle&#xff0c; Findbugs&#xff0c;PMD&#xff0c; 当然&#xff0c;还有我们最熟悉的…

洛谷 U3357 C2-走楼梯

https://www.luogu.org/problem/show?pidU3357 题目背景 在你成功地解决了上一个问题之后&#xff0c;方方方不禁有些气恼&#xff0c;于是他在楼梯上跳来跳去&#xff0c;想要你求出他跳的方案数。.. 题目描述 方方方站在一个n阶楼梯下面&#xff0c;他每次可以往上跳一步或两…

Liunx 系统调优

Sysctl命令用来配置与显示在/proc/sys目录中的内核参数&#xff0e;如果想使参数长期保存&#xff0c;可以通过编辑/etc/sysctl.conf文件来实现。 命令格式&#xff1a;sysctl [-n] [-e]-w # 临时改变某个指定参数的值&#xff0c;如sysctl -w net.ipv4.ip_forward1-a # 显示…

php多文件上传存储到表,PHP 实现一种多文件上传的方法

搜索热词之前在实现表单中file类型input选择多图片的时候找到一种方式 也许不是最好的但亲测可行且支持ie7以上以及chrome浏览器在表单中使用正常多文件选择multiple属性PHP;">然后使用AjaxFileUpload或其他方式提交将对应命名的file文件 $file[‘image] 转化为 json打…

CentOS7设置自定义开机启动,添加自定义系统服务

Centos 系统服务脚本目录&#xff1a; /usr/lib/systemd/ 有系统&#xff08;system&#xff09;和用户&#xff08;user&#xff09;之分&#xff0c;如需要开机没有登陆情况下就能运行的程序&#xff0c;存在系统服务&#xff08;system&#xff09;里&#xff0c;即&#xf…

成功应聘Intel的真实经历

编者按&#xff1a;INTEL&#xff08;英特尔&#xff09;公司创建于1968年&#xff0c;是全球最大的芯片制造商&#xff0c;Intel研究中心更是汇聚了全球无数的精英&#xff0c;一批年轻人抱着梦想走入了这里&#xff0c;过去我们谈到了太多关于Intel技术与市场方面&#xff0c…

Kotlin学习记录1

参考我的博客&#xff1a;http://www.isedwardtang.com/2017/09/02/kotlin-primer-1/转载于:https://www.cnblogs.com/EdwardTang/p/7476787.html

Keepalived配置文件详解

keepalivedkeepalived是集群管理中保证集群高可用的一个服务软件&#xff0c;其功能类似于heartbeat&#xff0c;用来防止单点故障。keepalived工作原理keepalived是以VRRP&#xff08;Virtual Router Redundancy Protocol&#xff0c;即虚拟路由冗余协议&#xff09;协议为实现…

php高等数学,中国大学《高等数学(四)》期末答案高校邦《PHP语言程序设计》见面课答案...

参考答案如下Conversation 2Pretco-A12.9-10.mp3:9、中国 A) Some shoes are missing. B) Itsdelivery is delayed.C) The order is cancelled. D) Some packages are damaged.10、中国 A) Giving an additional discount. B) Renewing the contract.C) Sending the goods by a…

深入剖析ThreadLocal实现原理以及内存泄漏问题

关于ThreadLocalMap<ThreadLocal, Object>弱引用问题&#xff1a; 当线程没有结束&#xff0c;但是ThreadLocal已经被回收&#xff0c;则可能导致线程中存在ThreadLocalMap<null, Object>的键值对&#xff0c;造成内存泄露。&#xff08;ThreadLocal被回收&#xf…

解读《普通大学应届毕业生如何成功应聘微软》

《普通大学应届毕业生如何成功应聘微软》这篇文章很有实践性&#xff0c;我所要提的&#xff0c;是最后一道面试&#xff0c;也就是唐骏本人对作者的面试&#xff0c;这一轮看似平常的面试大有门道。仔细想想&#xff0c;为什么这些问题由唐骏本人来问&#xff0c;他为什么要这…

grep 命令的 12 个实例

2019独角兽企业重金招聘Python工程师标准>>> 你是否遇到过需要在文件中查找一个特定的字符串或者样式&#xff0c;但是不知道从哪儿开始&#xff1f;那么,就请grep来帮你吧。 grep是每个Linux发行版都预装的一个强有力的文件模式搜索工具。无论何种原因&#xff0c;…